«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3. DevOps.. DevOps. github.. DevOps. github. Kubernetes.. DevOps. github. Kubernetes. n8n ai.. DevOps. github. Kubernetes. n8n ai. Программирование.. DevOps. github. Kubernetes. n8n ai. Программирование. Системное администрирование.

Привет! Мы в шаге от выпуска новой версии нашей игры. Число изменений в сервисах и инфраструктуре выросло кратно, все хотят уложиться в дедлайны. Каналы с алертами напоминают Красное море, а в канале поддержки всем не терпится узнать, почему упал билд, — и, конечно, немедленно его починить.

С диагностикой нам уже помогает агент из первой и второй частей — но это только полдела. Дальше начинается самое муторное: инженер должен понять, где именно нужно внести изменения, оценить, сможет ли он сделать их сам или тут нужен разработчик, найти нужный репозиторий среди десятков похожих, внести правку, прогнать её через PR и ревью. В релизный кранч эти «ещё пятнадцать минут» на каждое обращение складываются в часы, а переключение контекста добивает остатки концентрации. Знакомо? У нас это выглядело так: агент за две минуты выдаёт диагноз «в workflow не передаётся input окружения», а потом инженер ещё полчаса ищет, в каком из реюзабельных workflow это чинить.

Именно поэтому мы решили пойти дальше и дать возможность просто сказать:

«Эй, агент, исправь мой билд».

В этой части расскажу, как мы:

  • научили систему различать две фазы обращения — «анализ» и «исполнение» — и расширили классификатор execute-категориями;

  • добавили Postgres как хранилище состояния: теперь у каждого треда есть своя маленькая state machine;

  • перешли на Agentgateway для доступа к MCP-серверам — с JWT-авторизацией и разграничением инструментов;

  • выбирали способ запуска coding-агентов в Kubernetes;

  • построили два executor-workflow — для CI/CD и инцидентов — и коллбэк, который возвращает результат работы агента обратно в тред.

Все workflow и системные промпты выложены отдельно — ссылка в конце статьи.

Пример вызова агента
«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 1
«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 2
«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 3
«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 4

В предыдущих сериях

Коротко для тех, кто пропустил. У нас есть первая линия техподдержки на n8n: бот в Mattermost принимает обращения, классификатор с LLM раскладывает их по категориям, и дальше специализированные ветки разбираются с CI/CD-ошибками, инцидентами, вопросами по инфраструктуре и заведением задач в Jira. Всё это работает на MCP-инструментах (GitHub, Kubernetes, Grafana, DigitalOcean, Qdrant) и обходится примерно в 250$ в месяц. Подробности — в первой и второй частях.

Ключевое ограничение той системы: агенты были строго read-only. Они смотрели в логи, метрики и код, писали отчёты — но ничего не меняли. Теперь мы даём агенту руки: в случае проблем с CI/CD агент-executor может сам исправить код и открыть PR, а в случае инцидентов — перезапустить workload или поправить конфигурацию.

Забегая вперёд: «дать агенту руки» оказалось задачей на 20% про промпты и на 80% про обвязку. Потребовалось расширить классификатор, добавить базу данных, коллбэки и несколько новых workflow. Разберём всё по порядку.

Как теперь устроен полный цикл

Главное архитектурное изменение — у обращения появился жизненный цикл. Каждый тред в Mattermost соответствует одной задаче, которая проходит через состояния (это распространяется только на обращения по инцидентам и CI/CD):

«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 5

Полный путь обращения выглядит так:

  1. Первое сообщение в треде уходит в фазу анализа: read-only-агент из прошлых частей расследует проблему и постит отчёт.

  2. Если для исправления нужны изменения, задача переводится в статус verdict_ready, и система ждёт человека.

  3. Инженер читает отчёт в треде и отвечает «сделай» (или любой другой репликой с просьбой исправить) — обращение попадает в фазу исполнения.

  4. Executor-workflow запускает в Kubernetes джобу с coding-агентом, которая вносит изменения и открывает PR — либо аккуратно чинит что-то прямо в кластере.

  5. По завершении джоба дёргает вебхук, коллбэк закрывает задачу и постит финальный отчёт в тот же тред.

Фазу определяет статус в базе, а не модель. Первое сообщение в треде идёт в анализ; любое следующее сообщение, пока задача находится в verdict_ready, переклассифицируется в execute-вариант и уходит экзекутору. LLM предлагает категорию, но суффикс _execute вычисляется кодом из статуса. Галлюцинация модели не может ни запустить исполнение раньше времени, ни вернуть задачу с готовым вердиктом обратно в анализ.

Человек остаётся в петле. Ни одна задача не доходит до исполнения без явного ответа инженера в треде. Агент никогда не решает сам, что «пора чинить», — он предлагает, человек утверждает. Это дешёвый, но принципиальный предохранитель.

Хранилище состояния: Postgres

Пока агенты были read-only, состоянием можно было пренебрегать: упал workflow — ну и ладно, пользователь переспросит. Как только агент получает право вносить изменения, появляются вопросы, на которые без базы не ответить: не запустим ли мы двух агентов на одну задачу? Сколько раз можно ретраить упавшее исполнение? Кто, когда и почём что-то поменял?

Для этого мы завели таблицу agent_tasks во внешнем Postgres. Один тред — одна активная строка:

Схема
CREATE TABLE public.agent_tasks (
    task_id uuid DEFAULT gen_random_uuid() NOT NULL,
    thread_root_id text NOT NULL,        -- ключ корреляции: корневой пост треда
    channel_id text NOT NULL,
    requested_by text NOT NULL,
    task_type text NOT NULL,
    status text NOT NULL,
    repos jsonb DEFAULT '[]'::jsonb NOT NULL,
    verdict jsonb,                       -- отчёт анализа для людей
    agent_brief jsonb,                   -- машинный контракт для экзекутора
    fail_count integer DEFAULT 0 NOT NULL, -- бюджет ретраев
    k8s_job_name text,
    error text,
    -- аудит исполнения
    execution jsonb,
    execution_status text,
    execution_exit_code integer,
    execution_result text,
    execution_session_id text,
    execution_cost_usd numeric(12,6),
    execution_duration_ms bigint,
    pr_url text,
    created_at timestamp with time zone DEFAULT now(),
    updated_at timestamp with time zone DEFAULT now(),
    finished_at timestamp with time zone,
    CONSTRAINT agent_tasks_status_chk CHECK ((status = ANY (ARRAY['new'::text,
        'analyzing'::text, 'verdict_ready'::text, 'executing'::text,
        'pr_created'::text, 'done'::text, 'failed'::text, 'cancelled'::text])))
);

ALTER TABLE ONLY public.agent_tasks
    ADD CONSTRAINT agent_tasks_pkey PRIMARY KEY (task_id);

-- Один активный таск на тред. Гонки отсекаются на уровне БД, а не кода.
CREATE UNIQUE INDEX agent_tasks_active_thread_uniq ON public.agent_tasks
    USING btree (thread_root_id)
    WHERE (status <> ALL (ARRAY['done'::text, 'failed'::text, 'cancelled'::text]));

CREATE INDEX agent_tasks_thread_created_idx ON public.agent_tasks
    USING btree (thread_root_id, created_at DESC);

Пара деталей, которые здесь несут основную нагрузку:

  • CHECK-констрейнт на status — это и есть state machine. Невалидный переход просто не запишется.

  • Частичный уникальный индекс гарантирует, что на тред существует ровно одна незавершённая задача. Два сообщения подряд физически не смогут породить два параллельных исполнения — даже если в коде workflow где-то есть гонка.

  • fail_count — бюджет ретраев: упавшую задачу можно оживить следующим сообщением в треде, но не бесконечно.

  • Поля execution_* — аудит: стоимость, длительность, session id и ссылка на PR. Через месяц эксплуатации именно по ним считается, во сколько обходится вся затея.

Классификатор: две новые категории

В devopsChatAssistant из первой части добавились execute-категории:

  • ci_cd_error_execute — цепочка внесения исправлений по итогам анализа упавшего пайплайна;

  • incident_executor — цепочка внесения исправлений по итогам разбора инцидента.

Как я уже говорил, эти категории выбираются не моделью, а исходя из запроса в БД: если по треду уже есть задача в статусе verdict_ready, обращение уходит в execute-ветку. Схема работы классификатора превратилась в паттерн «generate → validate → act»: модель предлагает категорию, Code-нода перепроверяет её против строки в базе и переопределяет, если они расходятся. В логах execution при этом остаются отладочные флаги (_category_overridden, _model_category) — очень помогает разбираться, когда поведение системы кажется странным.

Ещё одно полезное обновление — идемпотентный SQL-запрос FindOrCreateAgentTask. Один стейтмент либо находит живую задачу треда, либо создаёт новую, либо оживляет упавшую (пока не исчерпан fail_count). Отдельной строкой — спасение задач, зависших в executing дольше 30 минут: экзекутор, умерший без записи терминального статуса, больше не блокирует тред навсегда. Эту логику мы дописали после первого же случая, когда джоба тихо упала по OOM, а тред «завис» до ручного UPDATE в базе.

Switch node
«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 6

Анализаторы: одно расследование — два документа

Ассистенты по CI/CD и инцидентам остались строго read-only, но теперь по итогам расследования пишут в базу два разных артефакта.

verdict — отчёт для людей. В нём сохраняются все гипотезы, которые рассматривала модель, доказательства по каждой, человекочитаемое описание фикса и допущения. Он нужен инженеру в треде сейчас — и тому, кто через три недели спросит, почему агент открыл вот этот странный PR.

agent_brief — машинный контракт для экзекутора. Ровно одна причина, один репозиторий, одна цель и самодостаточная инструкция:

{
  "version": 1,
  "category": "ci_cd_error",
  "objective": "fix(ci): pass the environment input to the deploy job",
  "instructions": "Пошаговое, самодостаточное описание изменения...",
  "risk": "low",
  "repo": "acme/backend-api",
  "extra_repos": [],
  "base_branch": "main"
}

Зачем это разделение? Мы быстро выяснили, что скармливать экзекутору полный отчёт с альтернативными гипотезами — плохая идея: агент начинает «выбирать» между вариантами и вести себя недетерминированно. Поэтому вердикт с рассуждениями остаётся людям, а исполнителю достаётся выжимка без права на интерпретацию. Важный нюанс: instructions пишутся так, будто у исполнителя нет доступа к переписке, — потому что его действительно нет.

Флаг needs_code_change, по которому задача уходит в verdict_ready, тоже выставляет не модель, а парсер: только если модель заявила о необходимости изменений и хотя бы одна причина с репозиторием и инструкциями пережила валидацию. Если изменений не нужно — задача закрывается как done, и тред освобождается.

У инцидент-ассистента в agent_brief появилось дополнительное поле action_kind с двумя значениями:

  • code_change — исправление живёт в репозитории: агент клонирует его, вносит правку и открывает PR;

  • k8s_operation — исправление живёт в кластере: перезапустить workload, откатить конфигурацию, поменять число реплик.

Agent Gateway

Ранее единой точкой входа к MCP-серверам у нас был Envoy Gateway. Решение рабочее, но не очень удобное: гибко разграничивать доступы к разным серверам и отдельным инструментам оно не позволяет. А как только у агентов появляется право на запись, вопрос «кто к какому тулу имеет доступ» перестаёт быть теоретическим.

Мы перешли на Agentgateway в связке с Envoy Gateway. Что он даёт:

  • аутентификацию по JWT-токенам или через IdP (Keycloak, Auth0 и т. п.);

  • авторизацию с точностью до отдельного инструмента — правила пишутся на CEL и могут опираться на клеймы токена;

  • федерацию нескольких MCP-серверов за одним эндпоинтом: инструменты автоматически получают префикс имени сервера (kubernetes_pods_get, grafana_get_datasource), и агенту можно выдавать группы тулов по серверам, к которым они относятся.

На практике это означает, что read-only-аналитик и executor ходят через один шлюз, но с разными токенами — и видят разные наборы инструментов.

Пример установки Agentgateway:

helm upgrade -i agentgateway-crds oci://cr.agentgateway.dev/charts/agentgateway-crds 
  --create-namespace --namespace agentgateway-system 
  --version v1.2.1 
  --set controller.image.pullPolicy=Always

helm upgrade -i agentgateway oci://cr.agentgateway.dev/charts/agentgateway 
  --namespace agentgateway-system 
  --version v1.2.1 
  --set controller.image.pullPolicy=Always 
  --set controller.extraEnv.KGW_ENABLE_GATEWAY_API_EXPERIMENTAL_FEATURES=true

У Agentgateway есть зависимость – Gateway API у вас должна быть установлена одна из реализаций,

Пример настройки:

manifests.yaml
---
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayParameters
metadata:
  name: tool-gw
  namespace: ai-infra
spec:
  service:
    spec:
      type: ClusterIP

---
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
  name: mcp
  namespace: ai-infra
spec:
  mcp:
    failureMode: FailOpen
    targets:
      - name: grafana
        static:
          host: grafana-mcp.ai-infra.svc.cluster.local
          port: 80
          protocol: StreamableHTTP

---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mcp
  namespace: ai-infra
spec:
  parentRefs:
    - name: tool-gw
      namespace: ai-infra
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /mcp
      backendRefs:
        - group: agentgateway.dev
          kind: AgentgatewayBackend
          name: mcp

Это минимальная конфигурация, чтобы заработал доступ. Сразу же стоит добавить AgentgatewayPolicy с аутентификацией через JWT или idP

В AgentgatewayBackend перечисляются все развернутые в кластере MCP сервера.

Ложка дёгтя — GitHub MCP. Нам нужно было прокидывать GitHub-токен клиента сквозь шлюз до MCP-сервера, но заголовок Authorization уже занят JWT самого agentgateway, и завести сквозную аутентификацию для этого кейса у нас не получилось. В итоге GitHub MCP подключён к агентам напрямую, мимо шлюза, — не самое элегантное решение, но рабочее. Если кто-то победил этот сценарий — расскажите в комментариях, честно, интересно.

Запуск агентов в Kubernetes

Теперь самое интересное: где и как запускать coding-агента, который будет вносить изменения. Мы смотрели на несколько вариантов:

  1. kube-foundry

  2. kagent

  3. обычные Kubernetes Jobs

kube-foundry

Kube Foundry — оператор, превращающий кластер в «фабрику» для coding-агентов: принимает задачи через CRD, поднимает под каждую изолированный под с агентом (Claude Code по умолчанию, поддерживаются также Codex и OpenCode), агент клонирует репозиторий, выполняет задачу и открывает PR.

Ставится в пару команд:

helm repo add kube-foundry https://kube-foundry.github.io/kube-foundry
helm repo update
helm install kube-foundry kube-foundry/kube-foundry 
  --namespace kube-foundry --create-namespace

kubectl create secret generic factory-creds 
  --namespace kube-foundry 
  --from-literal=ANTHROPIC_API_KEY=sk-ant-... 
  --from-literal=GITHUB_TOKEN=ghp_...

Задача описывается ресурсом SoftwareTask:

apiVersion: factory.factory.io/v1alpha1
kind: SoftwareTask
metadata:
  # имя задаёт и рабочую ветку: factory/pg17-change
  name: pg17-change
  namespace: ai-infra
spec:
  agent: claude-code
  repo: https://github.com/your-org/ansible-roles
  # базовая ветка для клонирования и таргет для PR
  branch: main
  task: Update ansible variable postgres_version to 17
  credentials:
    secretRef: factory-creds
  maxRetries: 1
  resources:
    cpu: "1"
    memory: 1Gi
    timeoutMinutes: 30

Дальше оператор сам ведёт задачу по фазам Pending → Running → Completed, а ссылку на PR кладёт в status ресурса. Для быстрого старта — отличный вариант с минимумом обвязки.

Почему в итоге обычные Jobs

К сожалению, готовым решениям не хватило гибкости под наши требования. Главная проблема — негде передать список разрешённых и запрещённых инструментов, а без этого контекст агента раздувается (об этом чуть ниже). Плюс в наших кейсах не всегда стабильно отрабатывали скачивание репозиториев и создание PR. kagent же ориентирован скорее на постоянно живущих в кластере агентов, а не на одноразовые задачи формата «склонируй — почини — открой PR».

Поэтому мы остановились на обычных Kubernetes Jobs: полный контроль над образом, entrypoint’ом, RBAC и жизненным циклом. Собрали собственный образ: Claude Code, git и gh, плюс entrypoint-скрипт, который клонирует репозиторий, запускает агента с промптом из переменной окружения, пушит ветку, открывает PR и по завершении дёргает вебхук с результатом.

Dockerfile

FROM node:24-trixie-slim

ARG IMAGE_VERSION
ENV IMAGE_VERSION=${IMAGE_VERSION}
ENV DEBIAN_FRONTEND=noninteractive

ENV ANTHROPIC_MODEL=anthropic/claude-opus-5

ARG GH_VERSION=2.97.0
ARG TARGETARCH

RUN apt-get update 
  && apt-get install -y --no-install-recommends 
    ca-certificates 
    curl 
    gettext-base 
    git 
    jq 
    less 
  && case "${TARGETARCH}" in 
       amd64) GH_ARCH=amd64 ;; 
       arm64) GH_ARCH=arm64 ;; 
       *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; 
     esac 
  && curl -fsSL "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" 
    | tar -xz -C /tmp 
  && mv "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh" /usr/local/bin/gh 
  && rm -rf "/tmp/gh_${GH_VERSION}_linux_${GH_ARCH}" 
  && rm -rf /var/lib/apt/lists/*

ARG CLAUDE_CODE_VERSION=2.1.220
RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"

COPY --chmod=0755 agent-entrypoint.sh /usr/local/bin/agent-entrypoint.sh

RUN mkdir -p /workspace && chown -R node:node /workspace
WORKDIR /workspace
USER node

ENTRYPOINT ["/usr/local/bin/agent-entrypoint.sh"]

Шаблон джобы (сокращённо; полная версия — в репозитории):

Job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: ${JOB_NAME}
  namespace: ${NAMESPACE}
  labels:
    app.kubernetes.io/name: claude-code-agent
    app.kubernetes.io/part-of: ai-infra
    devops-ai/task-type: ${TASK_TYPE_LABEL}
    devops-ai/thread-root: ${THREAD_ROOT_LABEL}
spec:
  backoffLimit: 0
  activeDeadlineSeconds: 1800
  template:
    metadata:
      annotations:
        devops-ai/task-id: ${TASK_ID}
    spec:
      restartPolicy: Never
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: agent
          image: registry.example.com/infra/claude-code-agent:v1.0.1
          env:
            - name: PROMPT
              value: ${PROMPT}
            - name: REPO_URL
              value: ${REPO_URL}
            - name: BASE_BRANCH
              value: ${BASE_BRANCH}
            - name: TARGET_BRANCH
              value: ${TARGET_BRANCH}
            - name: PR_TITLE
              value: ${PR_TITLE}
            - name: MCP_CONFIG_FILE
              value: /config/mcp.json
            - name: ALLOWED_TOOLS
              value: Read,Write,Edit,Grep,Glob,Bash,mcp__tool-gw__kubernetes_pods_get,mcp__tool-gw__kubernetes_pods_list,mcp__tool-gw__kubernetes_resources_get,actions_get,get_file_contents,search_code
            - name: PERMISSION_MODE
              value: acceptEdits
            - name: TIMEOUT_SECONDS
              value: "1500"
            - name: THREAD_ROOT_ID
              value: ${THREAD_ROOT_ID}
            - name: WEBHOOK_URL
              value: ${WEBHOOK_URL}   # коллбэк в n8n, URL — секрет
            - name: ANTHROPIC_API_KEY
              valueFrom:
                secretKeyRef: { name: agent-secret, key: ANTHROPIC_API_KEY }
            - name: GITHUB_TOKEN
              valueFrom:
                secretKeyRef: { name: agent-secret, key: GITHUB_TOKEN }
          resources:
            requests: { cpu: 500m, memory: 1Gi, ephemeral-storage: 10Gi }
            limits: { cpu: "1", memory: 2Gi, ephemeral-storage: 10Gi }
          volumeMounts:
            - { name: workspace, mountPath: /workspace }
            - { name: config, mountPath: /config, readOnly: true }
      volumes:
        - name: workspace
          emptyDir: {}
        - name: config
          configMap:
            name: agent-mcp-config

Джобе передаются все необходимые параметры, включая обратный вебхук для отправки результатов. Обратите внимание на backoffLimit: 0 и activeDeadlineSeconds: ретраить агента, вносящего изменения, — значит получить второе неконтролируемое изменение, а исправление, которое крутится полчаса, уже ничего не исправляет.

Грабли с числом инструментов

Про ALLOWED_TOOLS стоит сказать отдельно. Этот allowlist ограничивает, что агент может вызвать, — но описания всех инструментов, которые отдаёт MCP-сервер, всё равно загружаются в контекст. Ограничить сам список тулов на стороне Claude Code нельзя, поэтому резать его нужно на уровне agentgateway. Передача полного списка может привести к тому, что агент попросту не запустится, — у нас так и было с 320 доступными инструментами: контекст переполнялся ещё до первого полезного действия.

MCP конфиг подключается под общий ConfigMap

ConfigMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: agent-mcp-config
  namespace: ai-infra
data:
  mcp.json: |
    {
      "mcpServers": {
        "tool-gw": {
          "type": "http",
          "url": "http://tool-gw.ai-infra.svc.cluster.local:8080/mcp",
          "headers": {
            "Authorization": "Bearer ${MCP_GW_TOKEN}"
          }
        }
      }
    }

RBAC

И главный принцип всей исполнительной части: границей безопасности является RBAC, а не промпт. Инструкции вида «предпочитай обратимые операции» и «никогда не удаляй PVC» — это пожелания языковой модели, которая может ошибиться или быть переубеждена. Реальная граница — права ServiceAccount’ов, и их здесь два, у каждого своя роль:

  • ServiceAccount, под которым n8n создаёт джобы, — имеет create на batch/jobs в одном namespace и больше ничего;

  • ServiceAccount самого агента — выдаётся отдельной Role в целевом namespace: чтение состояния, patch/update на deployments и scale, delete на поды (так делается рестарт). Секреты, PVC и всё cluster-scoped отсутствуют намеренно.

Пишите эти права так, будто промпта не существует. Новый verb добавляется только тогда, когда реальный инцидент доказал, что без него никак.

CI/CD Executor

Когда в треде с задачей в verdict_ready появляется сообщение с просьбой внести предложенные изменения, запускается workflow DevopsCICDExecutor. Внутри него нет ни одного вызова LLM — всё детерминировано: захват задачи в базе, рендер манифеста, один REST-вызов к Kubernetes API. Интеллект уже отработал в фазе анализа; следующий интеллект запустится внутри джобы.

«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 7

Несколько решений, которые здесь отрабатывают за надёжность:

Атомарный захват задачи. Первый шаг — один запрос UPDATE ... WHERE status = 'verdict_ready' RETURNING *: это одновременно и чтение задачи, и её блокировка. Ноль строк в ответе означает, что параллельный запуск уже победил в гонке, — workflow просто тихо выходит, не запуская дубль.

Структурированные исходы вместо исключений. Каждый путь завершается одним из трёх исходов: ready (бриф валиден, запускаем джобу), failed (задача захвачена, но исполнить её нельзя — статус failed, fail_count + 1) или rejected (ничего не захвачено или джоба уже существует — база не трогается). Разница между failed и rejected не косметическая: fail_count — это бюджет ретраев, и сжигать его на гонку, которая даже не пыталась исполниться, было бы обидно.

Манифест как код. Шаблон джобы лежит в git, а не в JavaScript-ноде, и рендерится чистой подстановкой ${PLACEHOLDER}. ServiceAccount агента, секреты, лимиты и тег образа проходят ревью как любое другое инфраструктурное изменение — потому что отрендеренный манифест и есть вся власть агента. Валидация при рендере идёт в обе стороны: у каждого ${TOKEN} в шаблоне должно быть значение, и каждый обязательный плейсхолдер обязан присутствовать в шаблоне. Вторая проверка появилась не от хорошей жизни: шаблон с захардкоженным metadata.name прекрасно рендерится и падает только на втором запуске с HTTP 409 — и отлаживать это по логам n8n удовольствие ниже среднего.

Incident Executor

DevopsIncidentExecutor устроен по той же схеме, но ветвится по action_kind из брифа: code_change заканчивается pull request’ом, k8s_operation — проверенным состоянием кластера. Отличия от CI/CD-собрата:

  • захват задачи типизирован — ... AND task_type = 'incident', чтобы экзекуторы не могли увести задачи друг у друга;

  • activeDeadlineSeconds жёстче: исправление инцидента, работающее полчаса, уже ничего не исправляет — его нужно убить и вернуть человеку;

  • для k8s_operation цель (контекст, namespace, workload, критерий проверки) явно описывается в instructions — у агента нет доступа к переписке расследования, и додумывать ему нечем.

«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 8

Callback

Всё, что запускает долгоживущую джобу, рано или поздно должно услышать её результат. Наивный вариант — поллить Kubernetes API и скрести логи пода — хрупок: логи ротируются, поды собирает garbage collector, а сам цикл поллинга — ещё одна штука, которая умеет умирать. Поэтому джоба сама отчитывается о результате: по завершении работы агента (успешном или нет) entrypoint отправляет в n8n вебхук такого вида:

{
  "status": "success | failed",
  "exit_code": 0,
  "result": "Отчёт агента о проделанной работе (markdown)",
  "log_tail": "Последние строки лога — добавляются только при ошибке",
  "thread_root_id": "Ключ корреляции: по нему находится тред и строка в БД",
  "job": {
    "name": "claude-code-agent-incident-bc8498aa-msq5etlu",
    "pod": "claude-code-agent-incident-bc8498aa-msq5etlu-c52mk",
    "namespace": "ai-infra"
  },
  "repo": {
    "url": "Репозиторий, где происходили изменения",
    "base_branch": "main",
    "target_branch": "agent/claude-code-agent-incident-bc8498aa-msq5etlu",
    "pushed": true,
    "pr_url": "Ссылка на PR"
  },
  "usage": {
    "duration_ms": 98826,
    "num_turns": 16,
    "total_cost_usd": 1.491589,
    "session_id": "ec08771e-0e84-4063-9668-a9e5e8f3e756"
  }
}
«Эй, агент, исправь мой билд». Строим первую линию техподдержки на n8n. Часть 3 - 9

Принимающий workflow — маленький, но именно он замыкает петлю. Без него каждая задача навсегда остаётся в executing, и тред не принимает новых обращений. Несколько принципов, которые стоит утащить, даже если вы никогда не запустите coding-агента:

* Сначала корреляция, потом доверие. channel_id для ответа читается из строки в базе, а не из payload’а. Тот, кто вызвал вебхук, контролирует, что будет сказано, но не где.

* Терминальный статус — это освобождение блокировки. UPDATE идёт с условием WHERE status NOT IN ('done','failed','cancelled'): повторный коллбэк не перезакроет задачу и не перезапишет уже случившееся разрешение.

* Отчёт режется на куски. Отчёт агента легко превышает лимит поста Mattermost (~16 000 символов), поэтому билдер сообщений делит его по границам строк и постит цепочкой в тот же тред.

Что получилось

После пары месяцев жизни исполнительной части в бою:

  • Типичный прогон экзекутора — полторы-две минуты и 15–20 итераций агента; одно исправление обходится примерно в 1,5$ вызовов LLM.

  • Путь от «упал билд» до открытого PR — считаные минуты, без переключения контекста инженера: диагноз уже есть из фазы анализа, изменения приходят на ревью готовыми.

  • Совокупная стоимость системы подросла примерно до 400$ в месяц против прежних 250$. На фоне того, сколько инженерных часов в релизный кранч съедает рутина «найди репозиторий — внеси правку — открой PR», это по-прежнему небольшая плата.

Чего пока не удалось

По традиции — честный список узких мест, с которыми пока живём:

  • Сложные кейсы всё равно приходится исправлять вручную. Скорее всего, потому, что у агента нет полной картины происходящего: бриф самодостаточен, но узок, а контекст всей системы в него не помещается.

  • Нет лимита параллелизма. Десять тредов, дошедших до фазы исполнения одновременно, — это десять джоб в кластере. Пока страхуемся ResourceQuota на namespace, но честная очередь напрашивается.

  • Для k8s_operation цель описана прозой. Структурированного поля с целевыми ресурсами нет, поэтому экзекутор не может провалидировать, что агент остался в рамках, — тут вся надежда на RBAC.

Итог

Главный вывод этих двух месяцев: «дать агенту руки» — это в первую очередь инженерная обвязка, а не промпт-инжиниринг. State machine в базе, идемпотентные запросы, атомарные захваты, контракт между анализом и исполнением, RBAC как единственная настоящая граница — вот из чего состоит система, которой можно доверить кнопку «сделай». Сам агент в этой конструкции — почти сменная деталь.

И второй вывод: человек в петле — это не тормоз, а фича. Агент предлагает, инженер утверждает одним сообщением, изменения приезжают через PR под защищённую ветку. Скорость почти не теряется, а спать по ночам сильно спокойнее.

Все описаные манифесты и workflows доступны в репозитории https://github.com/javdet/automagicops-workflows

Автор: javdet12

Источник