Laney Docs
시스템

시스템 아키텍처

레이니의 자동화 시스템은 데이터베이스 이벤트를 감지하여 메시지 발송, AI 텍스트 생성, 브라우저 자동화까지 하나의 내구성 있는(durable) 워크플로우로 실행합니다. 서버가 재시작되어도 워크플로우가 이어서 실행되고, 클라이언트가 주기적으로 폴링하지 않아도 이벤트를 실시간으로 수신할 수 있는 구조입니다.


이 아키텍처를 택한 이유

기존 자동화 시스템에는 두 가지 문제가 있었습니다. 서버 크래시 시 워크플로우가 유실되는 문제, 그리고 클라이언트가 주기적으로 폴링해야 하는 비효율입니다.

항목기존 (폴링 기반)현재 (Hook + 이벤트 기반)
이벤트 감지클라이언트가 10초마다 DB 폴링PostgreSQL 트리거가 즉시 감지
실행 보장서버 크래시 시 워크플로우 유실내구성 스텝이 마지막 완료 지점부터 재개
브라우저 제어WebSocket 양방향 통신Hook suspend/resume 패턴
실시간 알림폴링 + WebSocket 혼합SSE 단방향 스트림 + Supabase Realtime
리소스 소비대기 중에도 연결 유지sleep/Hook 중 컴퓨팅 비용 0
재연결상태 유실 가능startIndex로 중간부터 이어받기

핵심 설계 원칙 4가지

내구성 (Durable Execution)

워크플로우는 "use workflow" 디렉티브로 선언됩니다. Vercel Workflow SDK가 각 스텝의 결과를 자동으로 저장하므로, 서버 재배포나 크래시 후에도 마지막 완료된 스텝부터 자동으로 재개됩니다. 이미 완료된 스텝은 캐시된 결과를 반환합니다(결정론적 재실행). LLM 호출이나 메시지 발송 같은 부수 효과가 중복 실행되는 것을 막아줍니다.

재개 가능 (Resumable)

sleep() 호출은 컴퓨팅 리소스를 소비하지 않고 워크플로우를 일시 정지합니다. 브라우저 에이전트의 createHook() 역시 외부 이벤트(클라이언트 스냅샷)를 기다리는 동안 워크플로우를 중단합니다. 두 경우 모두 대기 중에는 비용이 발생하지 않습니다.

관찰 가능 (Observable)

모든 스텝 시작/완료 이벤트가 내구성 스트림에 기록됩니다. 클라이언트는 SSE를 통해 실시간으로 이 스트림을 소비하며, startIndex 파라미터로 중간부터 재연결할 수 있습니다.

폴링 없음 (Zero-Polling)

이벤트 감지는 PostgreSQL 트리거가 담당하고, 실시간 알림은 Supabase Realtime(postgres_changes)이 처리합니다. 클라이언트가 주기적으로 폴링할 필요가 없습니다.


전체 아키텍처

시스템은 크게 4개 계층으로 구성됩니다.

  1. 이벤트 감지: PostgreSQL 트리거가 데이터 변경을 감지하고, dispatch_org_workflow() 함수가 필터 조건을 평가합니다
  2. 워크플로우 엔진: Vercel Workflow SDK 기반의 내구성 실행 엔진이 스텝을 순차적으로 실행합니다
  3. 스텝 실행: 각 스텝 타입(sleep, send_message, ai_generate 등)이 독립적으로 실행됩니다
  4. 결과 출력: 메시지 발송, 텍스트 생성, 브라우저 자동화 등 실제 작업이 수행됩니다

핵심 구성 요소

구성 요소기술역할
워크플로우 엔진Vercel Workflow SDK내구성 있는 실행, 재시작/재배포 후에도 이어서 진행
데이터베이스Supabase (PostgreSQL + Realtime)트리거 감지, 실행 기록 저장, 실시간 변경 알림
AI SDKVercel AI SDK + Google Gemini텍스트 생성(ai_generate), 브라우저 에이전트 의사결정
브라우저 클라이언트Python (laney-integrate)로컬 Chrome 제어, 스냅샷 전송, 커맨드 실행
스트림 계층Durable Stream + SSE실시간 이벤트 전달, 재연결 지원
대시보드Next.js + Supabase Realtime워크플로우 실행 상태 실시간 모니터링

실행 흐름 예시: 예약 확인 시나리오

고객이 예약을 확인(confirmed)하면 확인 메시지를 발송하는 워크플로우의 전체 흐름입니다.

단계설명기술
1. 데이터 변경예약 상태가 confirmed로 변경됨PostgreSQL UPDATE
2. 트리거 발동AFTER UPDATE 트리거가 dispatch_org_workflow() 호출PL/pgSQL
3. 필터 평가status 필드가 confirmed로 변경되었는지 확인evaluate_trigger_filters()
4. 실행 기록org_workflow_runs 테이블에 running 상태로 INSERTSupabase
5. HTTP 호출pg_net.http_post()로 워크플로우 엔진 API 호출pg_net
6. 워크플로우 시작start(executeWorkflow, payload)로 내구성 워크플로우 시작Vercel Workflow SDK
7. 대기sleep(30분) — 30분간 컴퓨팅 비용 없이 일시정지SDK sleep
8. 메시지 발송Bizgo OMNI API로 알림톡 발송HTTP API
9. 결과 저장최종 상태와 실행 시간을 DB에 기록Supabase

워크플로우 엔진 핵심

워크플로우 엔진은 두 가지 디렉티브로 내구성을 선언합니다.

  • "use workflow": 함수 본문 첫 줄에 선언하면 해당 함수가 내구성 워크플로우로 변환됩니다. 내부의 모든 "use step" 함수 호출은 결과가 자동으로 저장됩니다.
  • "use step": 함수 본문 첫 줄에 선언하면 해당 함수가 내구성 스텝으로 변환됩니다. 스텝의 반환값은 저장되어, 워크플로우가 재실행(replay)될 때 실제 로직을 다시 실행하지 않고 캐시된 결과를 반환합니다.

결정론적 재실행의 효과를 구체적으로 보면 이렇습니다.

시나리오재실행 없이재실행 (캐시)
LLM 텍스트 생성매번 다른 텍스트 생성, 비용 발생동일한 텍스트 반환, 비용 없음
메시지 발송중복 발송 위험발송하지 않고 성공 반환
DB 업데이트중복 업데이트업데이트하지 않고 성공 반환

스텝에 에러가 발생하면 이후 스텝은 실행되지 않습니다. 부분 실행 결과는 DB에 그대로 보존되어 어느 스텝까지 성공했는지 확인할 수 있습니다. 최종 상태는 success, error, partial 세 가지입니다.


AI 스텝 파이프라인

워크플로우 엔진은 Vercel AI SDK와 Google Gemini를 사용하여 두 가지 유형의 AI 스텝을 제공합니다.

ai_generate: 프롬프트를 기반으로 LLM에서 텍스트를 생성하는 범용 스텝입니다. 워크플로우 컨텍스트의 데이터를 {{data.field}} 형식의 템플릿 변수로 주입할 수 있어, 고객 데이터에 기반한 개인화된 텍스트를 생성합니다. "use step" 디렉티브가 적용되어 재실행 시 LLM을 다시 호출하지 않습니다.

모델속도적합한 용도
gemini-2.0-flash (기본)매우 빠름짧은 메시지 생성, 단순 분류
gemini-2.0-flash-lite가장 빠름템플릿 기반 단순 변환
gemini-2.5-flash-preview-04-17빠름복잡한 분석, 긴 텍스트 생성

템플릿 변수 문법:

형식설명예시 출력
{{data.field}}현재 행의 필드 값홍길동
{{previous_data.field}}변경 전 필드 값pending
{{steps.stepId.field}}이전 스텝 결과(생성된 텍스트)
{{data.field|date}}날짜 포매터 적용2026년 4월 2일 (수) 14:30

decideNextAction: 브라우저 에이전트 루프 내부에서 동작하는 AI 스텝입니다. generateText() 대신 generateObject()를 사용하여 Zod 스키마로 구조화된 JSON(done, reasoning, commands)을 출력합니다. 현재 페이지의 접근성 트리를 분석하여 다음 브라우저 명령을 결정합니다.


브라우저 에이전트 역할

브라우저 에이전트는 서버(워크플로우 엔진)와 클라이언트(Python + Chrome)가 Hook을 통해 협업하는 AI 기반 브라우저 자동화 시스템입니다.

워크플로우가 createHook()에서 완전히 일시정지(suspend)되고, 클라이언트가 스냅샷을 보내면 정확히 그 지점에서 재개(resume)됩니다. 이 사이에 서버는 어떤 컴퓨팅 리소스도 소비하지 않습니다.

LLM은 시각적 스크린샷이 아닌 접근성 트리(텍스트)를 분석하여 다음 행동을 결정합니다. Hook 토큰은 agent:{workflowRunId}:{stepId}:{stepNumber} 형식의 결정론적 구조를 가지므로, 서버 재시작 후에도 동일한 Hook을 정확히 매칭할 수 있습니다.


스트림 프로토콜 구조

워크플로우 엔진의 모든 이벤트는 내구성 스트림(durable stream)에 기록됩니다. 클라이언트는 SSE(Server-Sent Events)를 통해 실시간으로 소비하며, 연결이 끊어져도 startIndex로 중간부터 재연결할 수 있습니다.

WebSocket이 아닌 SSE를 선택한 이유는 워크플로우 이벤트가 서버에서 클라이언트로의 단방향 흐름이기 때문입니다. SSE는 HTTP/2 위에서 동작하여 CDN과 로드밸런서와 호환되고, startIndex로 상태 복원이 가능합니다.

청크 타입 목록:

타입발생 시점주요 필드
step:started스텝 실행 시작stepId, stepName, stepType
step:completed스텝 실행 완료stepId, status, output, durationMs
agent:ready브라우저 에이전트 루프 시작stepId, goal, maxSteps
agent:waitingHook 생성, 스냅샷 대기stepNumber, hookToken, deadlineAt
agent:actionLLM이 결정한 다음 행동actionId, commands, reasoning, done
agent:completed에이전트 루프 종료stepId, stepsExecuted
workflow:completed전체 워크플로우 완료status, durationMs

재연결 흐름은 단순합니다. 클라이언트가 마지막으로 수신한 인덱스를 lastIndex에 저장해두었다가, 재연결 시 GET /api/agent/{runId}/stream?startIndex={lastIndex}로 이미 받은 청크를 건너뜁니다.


클라이언트 통합 방식

워크플로우 엔진의 이벤트를 소비하는 클라이언트는 두 종류입니다.

  • Python 클라이언트 (laney-integrate): 로컬 환경에서 Chrome을 제어하며 워크플로우 엔진과 실시간으로 통신합니다. 새로운 워크플로우 실행은 Supabase Realtime(postgres_changes)으로 즉시 감지하고, Realtime 연결이 불안정한 환경을 위해 60초 폴백 폴링도 함께 동작합니다. SSE 스트림에서 agent:waiting 이벤트를 수신하면 Chrome에서 접근성 트리를 캡처하고 서버로 전송합니다.
  • 대시보드: Next.js로 구현된 실시간 모니터링 인터페이스입니다. org_workflow_runs 테이블의 변경을 Supabase Realtime으로 구독하여 실행 목록을 자동 갱신하고, useAgentStream React Hook이 SSE 스트림을 소비하여 스텝별 실행 상태와 AI의 의사결정 근거(reasoning)를 실시간으로 표시합니다.

두 클라이언트 모두 Supabase JWT 인증을 사용하며, 조직 수준의 접근 제어가 적용됩니다. 사용자가 속하지 않은 조직의 워크플로우 스트림에는 접근할 수 없습니다.


스텝 타입 요약

스텝 타입설명실행 위치대표 용도
sleep지정 시간 대기 (컴퓨팅 소비 없음)서버예약 30분 전 알림
send_message알림톡/친구톡/RCS/SMS 메시지 발송서버예약 확인 메시지
notification정보성 메시지 발송 (send_message 래퍼)서버예약 리마인더
advertisement광고성 메시지 발송 (send_message 래퍼)서버프로모션 안내
update_field데이터베이스 필드 값 수정서버고객 등급 변경
ai_generateLLM으로 텍스트 생성서버개인화된 메시지 작성
browser_agentHook 기반 AI 브라우저 자동화 루프서버 + 클라이언트외부 사이트 자동화

browser_agent 스텝은 다른 스텝과 달리 서버와 클라이언트가 협업합니다. 서버(워크플로우 엔진)가 LLM으로 다음 행동을 결정하고, 클라이언트(Python + Chrome)가 실제 브라우저를 조작합니다.


다음 단계

각 구성 요소의 상세 내용은 아래 문서에서 확인하세요.

On this page