본문으로 건너뛰기
Ankole

SignalsGateway

AI Agent용: 이 페이지의 Markdown 버전은 https://ankole.agentbull.com/ko-KR/docs/signals-gateway/index.md에 있습니다. 문서 색인은 https://ankole.agentbull.com/ko-KR/llms.txt에 있습니다.

SignalsGateway는 공유 작업의 진입 표면입니다. 한쪽에서 채팅 메시지, 웹훅, 프로바이더 이벤트, 예약된 알림이 들어오면, 다른 쪽에서는 세션을 깨울 준비가 된 정규화된 durable 액터 이벤트가 나옵니다. 게이트웨이의 역할은 다양한 프로바이더를 하나의 형태로 바꾸고, 원래 프로바이더 사실을 그 뒤에 따라오는 실행과 분리해 두는 것입니다.

이 페이지는 실제 수신 경로, 사용자용 라우팅 규칙 뒤에 있는 Signal Binding 모델, 그리고 미러링과 waking 사이의 경계를 설명합니다. 진실의 원천(source of truth)은 Ankole.SignalsGateway 모듈과 그 하위 모듈인 Ingress, Projection, Bindings입니다.

지키는 계약

모든 프로바이더에 공통으로 성립하는 두 가지 속성이 있으며, 이것이 게이트웨이가 별개의 계층으로 존재하는 이유입니다.

  • 소스 사실은 사실로 남습니다. 미러링된 항목(entry)은 프로바이더의 현재 모습 — 누가 어떤 채널에서 언제 무슨 말을 했는지 — 을 기록합니다. 이것은 실행 상태가 아닙니다. 에이전트의 turn은 이러한 사실 자체가 아니라 사실의 프로젝션(projection)을 대상으로 실행됩니다.
  • Waking은 조건부입니다. 수용된 모든 사실이 액터를 깨우는 것은 아닙니다. 필터링된 시그널은 성공적인 no-op(status: :filtered)이며, 액터 런타임은 수용된 사실이 실제로 새 액터 이벤트를 만들어냈을 때만 시작됩니다.

이 분리가 중요한 이유는 프로바이더마다 동작이 다르고, 재시도하거나 재전송하며, 에이전트가 보아야 하지만 반응할 필요는 없는 이벤트를 보내기 때문입니다. 게이트웨이가 이러한 차이를 흡수함으로써 액터 런타임은 하나의 깨끗한 입력 스트림만 보게 됩니다.

수신 파이프라인

모든 수신 사실은 프로바이더와 무관하게 동일한 고정 파이프라인을 거칩니다.

  1. 라우팅 규칙을 해석합니다. 게이트웨이는 agent_uid와 binding_name으로 내부 Signal Binding을 조회합니다. 규칙이 없으면 경로가 없으므로 해당 사실은 거부됩니다.
  2. 사실을 구성합니다. 프로바이더 고유의 페이로드는 FactNormalizer를 통해 entry, reaction, action, lifecycle 유형의 사실로 정규화됩니다. “delete”나 “recall” 같은 프로바이더별 이름은 액터가 마주하는 하나의 종류로 합쳐집니다.
  3. 라우팅 필터를 적용합니다. 규칙의 필터가 이 사실이 범위 안에 있는지 결정합니다. 일치하지 않으면 {:ok, %{status: :filtered}}를 반환하는데, 이것은 오류가 아니라 성공입니다.
  4. 수용하고 미러링합니다. 수용된 사실은 채널 미러를 upsert하고 항목 프로젝션을 기록합니다. 여기서 프로바이더 사실이 durable한 행이 됩니다.
  5. 필요할 때 액터 이벤트를 넘겨줍니다. 수용된 사실이 액터를 깨워야 한다면 ActorEvent 행이 세션 큐에 추가됩니다. 예외는 reaction입니다. reaction은 미러만 갱신하고 액터 이벤트를 만들지 않습니다.

수용 과정의 잠금 순서는 고정되어 있습니다. 채널, 그다음 세션, 마지막으로 액터 이벤트 순서입니다. 따라서 같은 채널의 동시 사실은 결정적으로 처리됩니다.

수신 사실의 종류

게이트웨이는 Ingress를 통해 네 가지 구체적인 종류를 수용하며, 각각은 정규화된 액터 지향 계약에 매핑됩니다.

  • Entry — 채널에 도착하는 메시지 또는 게시물. 기본적인 wake 경로입니다. IM 진입 정책은 수신자가 지정되지 않은 그룹 메시지가 may_intervene 이벤트(에이전트가 발언할 수 있음)를 만들지, addressed 이벤트(에이전트가 직접 호출됨)를 만들지 결정합니다. 개입 판단의 동작 방식, 답장 소속(reply attribution), 채널 상시 지시(standing order)는 주변 개입을 참고하세요.
  • Entry removed — 프로바이더의 삭제 또는 회수(recall). 액터 지향 계약은 항상 signal.entry.removed이며, 프로바이더 고유의 라이프사이클 이름은 진단용으로만 유지됩니다.
  • Reaction — 기존 항목에 대한 이모지 또는 투표 변경. 미러만 갱신하며 액터를 깨우지 않습니다. 게이트웨이가 미러링한 적 없는 항목에 대한 reaction은 무시됩니다(:ignored_unknown_entry). 오류로 취급되지 않습니다.
  • Action — 카드 버튼 클릭 같은 프로바이더가 발생시킨 상호작용. 답장 상호작용 중복 제거를 거칩니다. 중복 클릭은 :duplicate_action, 오래된 클릭은 :stale_action을 반환하고, 수용된 클릭은 signal.action.invoked 이벤트가 됩니다.

채널, 항목, 답장 모드

channel은 항목이 존재하는 프로바이더 쪽 컨테이너입니다. IM 다이렉트 메시지나 그룹, 웹훅 엔드포인트, 이슈, 알림 스트림이 그 예입니다. 채널 행은 프로바이더 고유의 채널 id를 키로 하는 순수한 외부 사실 미러이므로, 이벤트마다 insert하는 대신 upsert로 기록됩니다. 채널 행은 reply_mode — :none, :channel, :entry — 를 기록하며, 아웃박스는 이 값을 읽고 답장을 새 채널 게시물로 보낼지 특정 항목에 대한 스레드 답장으로 보낼지 결정합니다.

entry는 채널 안의 콘텐츠 한 단위입니다. 메시지 하나, 게시물 하나, 이벤트 하나가 그 예입니다. 에이전트 turn이 읽는 것은 항목 프로젝션입니다. 액터 이벤트도 아니고, 소스 페이로드를 그대로 둔 것도 아닙니다.

라우팅 규칙 모델

Signal Binding으로 저장되는 시그널 라우팅 규칙은 운영자가 선택한 이름 아래에서 하나의 프로바이더 어댑터를 하나의 Agent에 연결합니다. 규칙은 Agent에 속하며 Console 범위의 라우트로 관리됩니다.

메서드 경로 용도
GET /signal-adapters 이 배포 인스턴스가 선언한 어댑터 목록
GET /signal-bindings 라우팅 규칙 목록(?agent=로 Agent 필터)
PUT /agents/:agent_uid/signal-bindings/:adapter_id/:binding_name 라우팅 규칙 생성 또는 교체
PATCH /agents/:agent_uid/signal-bindings/:binding_name 라우팅 규칙 갱신
DELETE /agents/:agent_uid/signal-bindings/:binding_name 라우팅 규칙 제거

규칙은 사용하는 어댑터, 구성 참조, 필터 규칙, 그룹 메시지 정책, enabled 플래그를 담습니다. 규칙을 비활성화하면 규칙을 삭제하지 않고도 새 사실이 해당 Actor를 깨우지 않게 됩니다. 사용할 수 없게 된 규칙은 unavailable_reason을 기록하므로 운영자는 규칙이 멈춘 이유를 확인할 수 있습니다.

어댑터는 하드코딩되지 않습니다. 부팅 시 signals_gateway.adapter 계약 아래 플러그인 레지스트리에서 해석되므로, 사용 가능한 프로바이더 집합은 이 배포 인스턴스의 플러그인이 선언하는 대로 결정됩니다. 어떤 선언도 제공하지 않는 어댑터 id에 대한 요청은 signal_adapter_not_found를 반환합니다.

프로바이더 웹훅 수신

이벤트를 푸시하는 프로바이더는 하나의 정문을 통해 게이트웨이에 도달합니다.

POST /webhooks/v1/:handler_id/:instance_id/:kind

이 라우트는 의도적으로 모든 인증 파이프라인밖에 있습니다. 세션도, CSRF도, bearer 토큰도, Accept 협상도 없습니다. 프로바이더는 자기 방식대로 헤더를 보내기 때문입니다. 프로바이더를 인증하는 일은 선언된 핸들러의 몫입니다. Bot Framework JWT, Graph clientState, 또는 프로바이더가 서명에 사용하는 무엇이든 그것입니다.

컨트롤러는 라우팅만 합니다. 선언된 signals_gateway.webhook_handler 플러그인을 해석하고, 핸들러가 선언한 kind 화이트리스트를 적용하며, 핸들러의 응답 지시를 렌더링합니다. 알 수 없는 핸들러나 선언되지 않은 kind는 페이로드를 되돌려 보내지 않고 404를 반환하고, 실패한 핸들러는 일반 메시지와 함께 500을 반환합니다. 정규화된 사실로 Ingress를 호출하는 것은 핸들러 자신입니다.

Agent가 만든 웹훅 위임

Agent가 만든 콜백 기능은 별도의 수신 경로를 사용합니다.

POST /webhooks/v1/event-callbacks/wh_<token>

이 경로는 프로바이더 핸들러를 사용하지 않습니다. URL 자체가 그것을 만든 Agent 세션으로 가는 하나의 wake-up 경로를 승인합니다. 본문의 사실을 인증하지는 않습니다.

SignalsGateway는 토큰 다이제스트만 저장합니다. 엔드포인트를 잠그고, 원샷(one-shot) 또는 상시(standing) 전달 의미론을 적용하며, 같은 PostgreSQL 트랜잭션 안에서 webhook.received를 추가합니다. 그러면 Worker가 신뢰할 수 없는 데이터 경계 안에서 제한된 헤더와 본문 데이터 집합을 프로젝션합니다. Agent는 결과에 영향을 주는 행동을 하기 전에 현재 외부 시스템을 읽습니다.

엔드포인트 생성, 목록, 취소 명령은 turn 로컬 Worker 브리지를 사용합니다. Console은 엔드포인트를 나열하고 취소할 수 있지만, 생성하거나 콜백 URL을 공개할 수는 없습니다. 사용자 흐름, GitHub 라이프사이클, 전달 계약, 운영상 한계는 Webhook 위임을 참고하세요.

아웃박스: 바깥으로 나가는 답장

수신은 게이트웨이의 절반입니다. 나머지 절반은 아웃박스로, 에이전트의 답장을 프로바이더 쪽 전송으로 바꿉니다. 아웃박스는 채널의 reply_mode를 읽고 전송 작업 — 채널 게시물 또는 스레드 항목 답장 — 을 선택하며, 수신이 사용하는 것과 같은 어댑터 계약을 거칩니다. 에이전트가 turn 중에 커밋한 부수 효과는 이 경로로 전달되며, 그 durable 기록은 라이브 Worker가 아니라 그것을 만들어낸 액터 이벤트와 함께 보관됩니다.

SignalsGateway가 아닌 것

SignalsGateway는 프로바이더 클라이언트가 아닙니다. 모든 채팅 플랫폼에 열린 연결을 유지하지 않습니다. 그것은 어댑터와 어댑터의 장기 연결 Worker 몫입니다. 임의의 작업을 위한 큐도 아닙니다. 세션의 액터 지향 이벤트만을 위한 큐입니다. 그리고 에이전트 실행이 일어나는 곳도 아닙니다. 액터 이벤트가 추가되고 나면, 세션을 깨우고 turn을 실행하는 것은 Actor Runtime의 몫입니다. 게이트웨이의 경계는 프로바이더 사실을 durable 액터 입력으로 바꾸는 변환과, 액터 답장을 프로바이더 전송으로 다시 바꾸는 변환입니다.

다음 단계