본문으로 건너뛰기

server ↔ ai 인터페이스

갱신일 2026-07-26 · 기준: server@bf192cc · ai@60ee59a + openapi3-ai.yml · asyncapi-server-ai.yml · openapi3-server.yml 공통 규칙(인증·오류·식별자·시간)은 common-conventions.md에 있다. 흐름의 시퀀스 원문은 agent-chat-flow.md가 정본이다. 여기서는 경계 규칙만 적는다.

브라우저는 이 구간에 절대 닿지 않는다. heymoa-ai는 내부 전용이라 public edge도 CORS도 갖지 않는다.


확인한 사실

0. 경계 전체

server → ai 3경로는 인증이 없고, ai → server 2종은 인증이 있다. 이 비대칭이 §4다.

1. 분석 — 202 + callback

브로커가 없다. DB row가 큐다.

경계 규칙 다섯:

  1. 202는 접수이지 완료가 아니다. 결과는 반드시 callback으로 온다. 같은 analysisId 재요청은 진행 중이든 완료됐든 동일하게 202이고, 이미 완료됐으면 새 작업을 만들지 않고 callback을 재발송한다.
  2. 멱등성 키는 analysisId이고 server가 발급한다. ai는 이 값으로 LLM 재실행을 막고, server는 이 값으로 중복 callback을 무시한다. 양쪽이 같은 키를 본다.
  3. callbackUrl은 요청 payload가 실어 보낸다. ai가 자기 설정값과 origin을 대조해 다르면 400 — 임의 주소로 결과가 나가지 않는다.
  4. 전사는 payload에 통째로 push한다. 채팅(§3)과 반대다.
  5. 재시도·워치독은 server 단독 소유다. ai의 큐가 인메모리라 프로세스가 죽으면 접수분이 사라지지만, 워치독(1분 주기/10분 타임아웃)이 덮고 멱등성 캐시가 중복 분석을 막는다. ai는 무상태 재시작이 가능해야 한다.

요약 3종은 파싱하지 않는 markdown 문자열로 넘어온다. server는 그대로 저장하고 web이 렌더한다 — 구조화 책임이 어느 쪽에도 없다는 것이 계약의 선택이다.

2. 채팅 — SSE passthrough

server는 중계자이지 변환기가 아니다.

  • ai가 POST /internal/v1/agent-chats/{chatId}/messages의 응답 본문으로 SSE를 스트리밍하고, server가 변환 없이 web으로 통과시킨다.
  • 그래서 이벤트 정의를 두 곳에 두면 반드시 갈라진다. 단일 출처는 asyncapi-web-server.yml이고 asyncapi-server-ai.yml이 이를 $ref한다 — 실제 코드로 검증되는 쪽이 web 구간이기 때문이다.
  • server는 이벤트 단위가 아니라 입력 행(line) 단위로 읽는다. 행마다 클라이언트 취소를 확인하고 공유 챗의 입력 잠금을 갱신한다. 그래서 comment만 흐르는 구간도 server에겐 "살아 있는" 신호이고, 반대로 행이 60초간 없으면 유휴로 판단해 끊는다.

server가 스트림에서 tee하는 것은 셋이다.

tee언제회의 ACTIVE 게이트
USER 메시지요청 시걸린다
message_end.content (ASSISTANT 전문)정상 종료 시걸린다
승인·도구 실행 기록 (TOOL)tool_approval_resolved · tool_call_result 수신 시걸리지 않는다

TOOL tee에 게이트를 태우면 안 되는 이유 — 승인 직후 회의가 끝나면 외부 시스템은 바뀌었는데 히스토리에 흔적이 없어 감사가 불가능해진다. 이미 일어난 사실의 기록이라 회의 상태에 종속되지 않는다.

승인 흐름의 경계:

  • 쓰기 도구만 승인을 거친다. 조회 도구는 자동 실행된다.
  • 승인 주체는 해당 메시지의 입력자 본인이다. 도구는 워크스페이스 연동 계정 명의로 실행되지만 동의는 입력자가 한다.
  • server는 재개 직전에 현재 멤버십을 다시 검증한다 — 요청 시점 권한만 믿으면 그 사이 탈퇴한 유저가 도구 실행을 승인할 수 있다.
  • ai는 권한을 재검증하지 않는다. server가 검증을 마쳤다고 전제한다.
  • ai는 도구 실행 완료를 기다리지 말고 즉시 응답한다. 호출자 read timeout이 10초이고 결과는 열려 있는 SSE로 가므로, 이 응답이 실행 완료를 뜻할 필요가 없다.
  • 단절 시 run이 죽는다 — server가 SSE 연결을 끊으면 ai는 진행 중 run을 취소하고 대기 중 interrupt를 폐기한다. 그 뒤 도착하는 재개 요청은 404이고 도구는 실행되지 않는다. 스트림이 끝난 뒤 도구만 실행돼 이력 없이 외부가 바뀌는 창은 존재하지 않는다 (APP-142 확정).

3. 컨텍스트 — push가 아니라 pull

전사·요약은 채팅 요청 payload에 싣지 않는다. ai가 server의 내부 조회 API로 당겨간다.

scope무엇을 당기나어떻게 쓰나
noteGET /internal/v1/notes/{noteId}/context전사+요약을 통째로 주입
workspaceGET /internal/v1/workspaces/{workspaceId}/notes노트 목록을 인덱스로 agentic 검색 도구 사용
  • 매 턴 다시 당긴다. 진행 중 회의면 그 시점 스냅숏이 반영되고, 전사는 대화 히스토리에 쌓이지 않는다 — checkpoint에는 메시지만 남는다.
  • 두 조회 경로 모두 X-Internal-Token이 필요하다. 방향이 ai → server라 필터가 걸린다.
  • 대화 full state는 ai의 LangGraph checkpointer가 chatId 기준으로 유지한다. server는 히스토리를 다시 보낼 필요가 없고, ai에는 히스토리 조회 API가 없다 — 표시·히스토리용 사본은 tee한 server 쪽에만 있다.

push와 pull이 흐름마다 다른 것은 의도다. 분석은 한 번에 끝나는 배치라 payload가 자연스럽고, 채팅은 멀티턴이라 매번 실어 보내면 checkpoint가 전사로 부풀고 진행 중 회의의 최신 상태도 반영되지 않는다.

4. 인증 비대칭

방향인증확인된 사실
ai → server /internal/**X-Internal-Token 공유 시크릿필터가 경로 단위·기본 차단이라 새 internal 경로가 자동 보호된다 (APP-122)
server → ai없음server의 세 클라이언트(채팅 SSE·승인 재개·분석 디스패치) 어디에도 헤더를 붙이는 코드가 없다. 네트워크 격리가 유일한 경계다

코드와 계약은 일치한다 — 양쪽 다 "헤더 없음"이다. 다만 계약 파일이 스스로 이를 미결로 등재했다 (openapi3-ai.yml info.description).

대칭으로 바꾼다면 양쪽을 같은 변경으로 맞춰야 한다. server의 세 클라이언트가 헤더를 실기 전에 ai에서 검증을 켜면 모든 호출이 401이 되고 분석과 채팅이 전부 죽는다.

5. 스트림 종료 — 이 구간에서는 계약 위반이다

common-conventions.md §8의 종료 규약 중 이 구간에만 해당하는 것:

  • ai는 반드시 message_end 또는 error로 끝낸다. 종료 이벤트 없이 연결이 닫히면 server가 실패로 처리하고 응답을 저장하지 않는다. web 구간에서는 정상 시나리오인 "종료 이벤트 없이 끊김"이 여기서는 계약 위반이다.
  • message_end.content는 비어 있지 않은 문자열이어야 한다. 비면 server가 저장을 건너뛰는 게 아니라 스트림을 실패시킨다.
  • approvalId는 13자 TSID여야 한다. 아니면 승인 row가 등록되지 않아 카드는 뜨는데 승인 API가 404가 되고 300초 대기 후 REJECTED로 끝난다 — 조용히 깨지는 경로다.
  • toolCallId는 시작 이벤트와 결과 이벤트에서 짝이 맞아야 한다. 어긋나면 도구 이름이 unknown으로 남는다.

미결

  1. 인증 비대칭의 의도 여부가 정해지지 않았다 (§4). 계약이 미결로 등재한 상태이고 결정 주체가 지정돼 있지 않다.
  2. chatKind를 server가 보내지 않는다. 계약(openapi3-ai.yml)은 이 필드가 agent profile을 정한다고 규정하고 optional·기본값 personal로 두었으나, 같은 파일이 "현재 heymoa-server는 이 필드를 보내지 않는다(요청 DTO에 없다)"고 적어 두었다. ai가 받는 값은 항상 personal이므로 공유 챗봇 profile이 선택되지 않는다. scope만으로는 갈리지 않는다는 것이 계약의 전제이므로 이 공백은 기능 결함이다 — APP-172로 MVP2에 이월됐다. 원장: pm/2-product/open-issues.md.
  3. ANALYSIS_WORKERS와 provider rate limit의 관계가 미측정이다. 동시 LLM 요청이 이 값의 3배까지 간다는 것만 코드로 확인됐다.
  4. 운영 base-url·callback-base-url 실제 주입값을 확인하지 못했다. 저장소에는 로컬 기본값만 있고 prod 오버라이드가 없다. 이 구간의 두 값(server → ai base-url, ai → server callback-base-url)이 어긋나면 분석 결과가 저장되지 않으므로, 배포 형상 미확인 항목과 함께 추적한다 → architecture/aws-architecture.md "미확인 — 저장소에 없는 배포 경로".

이어서 볼 것어디
인증·오류·식별자·시간 공통 규칙common-conventions.md
이 SSE가 브라우저까지 가는 구간server-web.md
채팅 루프 시퀀스와 경계 규칙 원문agent-chat-flow.md
스키마·필드·타임아웃 값openapi3-ai.yml · asyncapi-server-ai.yml
각 서비스 내부 구조server/README.md · ai/README.md