콘텐츠로 이동
Study NoteAgent 배포 플랫폼

kagent 0.x 채팅을 Next.js 포털에 포팅하기

결론부터
기존 Next.js에 kagent의 채팅 코드를 옮기고, 사내 백엔드가 권한을 검사한 뒤 Controller API를 호출하며 대화 저장은 kagent에 맡긴다

임직원이 기존 포털에서 공유된 에이전트를 선택하고, 각자 대화를 만들고, 다음 날 같은 대화를 이어 가려 한다. 포털의 로그인·메뉴는 이미 있으므로 대화 목록과 채팅 기능을 가져와 한 메뉴 안에 넣는 방식을 사용한다. 이 페이지는 어떤 코드를 가져오고, 어떤 API를 연결하며, 사용자별 기록이 분리되는지 어떻게 확인할지 설명한다.

이 장에서 처음 나오는 말4개
Controller API
kagent가 제공하는 HTTP 입구다. 사내 백엔드는 이 API로 에이전트와 대화를 조회한다.
Session
한 사용자가 한 에이전트와 나눈 대화 묶음이다. kagent가 ID와 소유자를 저장한다.
A2AAgent-to-Agent
메시지를 보내고 실행 상태와 결과를 받는 프로토콜이다. 여기서는 JSON-RPC 방식으로 사용한다.
SSEServer-Sent Events
서버가 열린 HTTP 연결로 응답 조각을 계속 보내는 방식이다. 채팅의 실시간 표시에 사용한다.

이 구현안은 kagent를 사용하는 경우의 포팅 방법이다. 덱 전체의 runtime 선정 결론을 바꾸지는 않는다. 사내 frontend·backend와 연결하기가 제품 중립 계약과 CRD 관리 경로를 설명한다면, 여기서는 kagent의 기존 UI용 조회·세션 API에 의존하는 범위를 명시하고 재사용한다. 이 API를 장기 호환성이 보장된 사내 공개 계약으로 취급하지 않고, 백엔드의 연동 모듈 안에 둔다.

항목이 페이지의 기준
소스kagent-dev/kagent의 v0.10.1, commit a3d26eaf2eda002d9c9104d2a8f6f27a2ff23ec3
UINext.js 16·React 19, App Router·Server Actions를 사용하는 앱
첫 적용 대상일반 Agent의 A2A 채팅. SandboxAgent·AgentHarness·ACP 경로는 별도 확장
가정한 대상 환경기존 Next.js 프런트엔드·백엔드, Kubernetes 1.35 계열, 사내 인증 체계
검토 범위공식 소스의 경로·동작을 확인한 설계 참조. 실제 클러스터 설치나 포팅은 검증하지 않음

UI의 기술 스택은 고정 버전의 package.json에서 확인할 수 있다. 기존 포털의 Next.js·React 버전과 App Router 사용 여부는 적용 전에 대조한다. 이 문서를 이유로 포털 전체를 무조건 업그레이드하지 않는다. kagent의 main은 1.x 코드일 수 있으므로 가져올 소스도 고정한다.

기존 Next.js 포털이 사내 백엔드를 거쳐 kagent Controller에 접근하고, 회사 권한은 포털 DB에 대화 기록은 kagent DB에 저장하는 구조

사내 백엔드가 연결할 곳은 Controller의 HTTP API다. 응답 stream은 같은 호출 경로를 거슬러 포털로 돌아온다. kagent DB의 테이블을 직접 조회하거나 수정하지 않는다. Controller의 세션 처리에는 사용자 구분, 공유 토큰에 따른 조회, 이벤트·task 조회가 이미 들어 있다. 세션 핸들러가 그 처리를 맡으므로 DB 스키마와 조회 규칙을 회사 코드에 복제할 필요가 없다.

저장 위치원본으로 관리할 데이터
포털 DB회사 Agent ID·소유자·버전·공개 범위·개인/부서 권한·감사, 회사 ID와 kagent 배포 위치의 연결
kagent DB직원별 세션·제목·대화 및 실행 기록
포털 DB의 선택 확장즐겨찾기·폴더, 제품 중립 conversation ID가 필요할 때의 kagent session ID 매핑

기본적인 내 대화 목록만 위해 직원↔대화 테이블을 다시 만들 필요는 없다. 다만 kagent DB가 대화의 원본이므로 백업·보존·삭제 정책을 함께 운영한다. 감사용 메타데이터를 남기는 것과 대화 본문을 별도 보관하는 것은 다른 결정이다. 이 페이지에서는 대화 본문 이중 저장을 기본으로 두지 않는다.

모델 호출은 Agent에서 사내 LiteLLM으로, 도구 호출은 Agent에서 MCP 서버로 간다. kmcp는 MCP 서버의 배포·운영을 돕는 자리이며 대화 목록이나 채팅 프록시를 맡지 않는다. 기반 연결은 LiteLLM과 사용자 MCP 공급 계약을 따른다.

포털에는 AI 에이전트 메뉴를 추가한다. 기존 로그인·루트 레이아웃·배포 파이프라인을 사용하고, 아래 기능을 하나의 모듈로 옮긴다. 독립 npm 위젯을 설치하는 작업이 아니라 소스와 의존성을 이식하는 작업이다.

v0.10.1/ui/src/의 출발점포팅할 역할사내에서 조정할 부분
components/sidebars/SessionsSidebar.tsx에이전트별 대화 선택회사 에이전트 목록과 포털 URL
components/chat/ChatLayoutUI.tsx대화 사이드바·채팅 Context포털 레이아웃에 맞춘 배치·불필요한 관리 패널 제거
components/chat/ChatInterface.tsx입력·히스토리 복원·스트리밍·승인서버 호출·라우팅·사용할 기능
components/chat/ChatMessage.tsx, ToolCallGroup.tsx, ToolCallDisplay.tsx, AskUserDisplay.tsx메시지·도구 호출·추가 질문 표시사내 디자인 컴포넌트와 스타일
app/actions/sessions.ts세션 생성·조회·이름 변경·삭제사내 백엔드 호출로 교체
lib/a2aClient.ts, lib/messageHandlers.tsstream 파싱·task를 화면 메시지로 변환포털의 stream URL과 오류 처리

ChatInterface의 import를 따라 필요한 types·Context·하위 컴포넌트를 함께 가져온다. 위 표는 독립 실행에 충분한 파일 목록이 아니라 수정 시작점이다. 원본 라이선스와 저작권 고지, 가져온 버전·수정 이력을 모듈 옆에 남긴다. 프로젝트 라이선스도 함께 보관한다.

다음은 새로 정할 사내 구조의 예시다. kagent가 제공하는 디렉터리 구조와 구분한다.

  • 디렉터리src/
    • 디렉터리app/
      • 디렉터리ai-agents/
        • page.tsx 에이전트 목록
        • 디렉터리[agentId]/
          • 디렉터리chat/
            • page.tsx 새 대화
            • 디렉터리[sessionId]/
              • page.tsx 기존 대화
    • 디렉터리features/
      • 디렉터리agent-chat/
        • 디렉터리components/ 이식한 채팅 화면과 필요한 Context
          • …
        • 디렉터리api/ 사내 백엔드 호출과 stream 처리
          • …
        • types.ts
        • UPSTREAM.md 가져온 버전·출처·변경 내역

실제 서버 호출은 포털의 기존 방식에 맞춘다. Server Actions와 Route Handler를 사용한다면 브라우저 → Next.js 서버 → 사내 백엔드 → Controller 순서이며, Next.js 서버는 화면용 중계만 맡는다. 권한의 최종 판단은 사내 백엔드에 둔다.

포팅할 때 다음 세 지점을 함께 고친다.

  • @/components, @/types 같은 import와 전역 CSS를 포털에 맞춘다. 원본의 루트 레이아웃·인증 Provider를 통째로 덮어쓰지 않는다.
  • 원본의 /agents/.../chat/... 이동 링크와 useParams()의 chatId를 새 경로에 맞춘다. 생성 직후 URL 변경·새로고침·뒤로 가기도 포함한다.
  • 기존 채팅 layout은 전체 에이전트와 MCP 서버를 읽는다. 포털에서는 사용 가능한 에이전트와 화면에 필요한 정보만 받도록 바꾼다.

조회와 스트리밍의 두 연결 지점

섹션 제목: “조회와 스트리밍의 두 연결 지점”

원본 UI의 일반 조회는 app/actions/utils.ts의 fetchApi()를 거친다. 스트리밍은 브라우저가 /a2a/{namespace}/{agentName}을 호출하면 Next.js Route Handler가 Controller의 /api/a2a/...로 넘긴다. 일반 조회 코드와 A2A Route Handler를 각각 사내 백엔드 호출로 바꾼다. 한쪽만 바꾸면 채팅이나 히스토리 조회가 사내 권한 검사를 우회할 수 있다.

원본 앱을 별도로 배포할 때 쓰는 ui.image.*, Nginx·Supervisor 설정은 이식한 포털에 필요하지 않다. 대신 기존 포털의 Ingress와 Route Handler가 SSE를 버퍼링하지 않고 전달하도록 맞춘다. 기존 프런트엔드 이미지에 채팅 모듈을 포함해 배포한다.

사내 백엔드와 Controller의 API 매핑

섹션 제목: “사내 백엔드와 Controller의 API 매핑”

사내 백엔드에는 KagentChatClient 같은 버전별 연동 모듈을 둔다. Controller의 주소는 서버 설정으로 고정한다. 기본 release·namespace가 모두 kagent라면 주소 예시는 http://kagent-controller.kagent.svc.cluster.local:8083이며, 실제 설치의 Service 이름을 확인해 사용한다. 브라우저가 보낸 URL을 upstream 주소로 사용하지 않는다.

아래 왼쪽 열은 제안하는 사내 API, 오른쪽 열은 확인한 kagent 0.10.1 API다.

사내 API 예시Controller에 보내는 요청
GET /api/ai/agentsGET /api/agents, 필요하면 ?namespace=...
GET /api/ai/conversationsGET /api/sessions
GET /api/ai/agents/{agentId}/conversationsGET /api/sessions/agent/{namespace}/{agentName}
POST /api/ai/agents/{agentId}/conversationsPOST /api/sessions
GET /api/ai/conversations/{sessionId}GET /api/sessions/{sessionId}
GET /api/ai/conversations/{sessionId}/historyGET /api/sessions/{sessionId}/tasks
PATCH / DELETE /api/ai/conversations/{sessionId}PATCH / DELETE /api/sessions/{sessionId}
POST /api/ai/conversations/{sessionId}/streamPOST /api/a2a/{namespace}/{agentName}/

대응하는 경로는 HTTP router에 있다. 목록 API는 Controller가 조회 가능한 범위를 돌려준다. 사내 백엔드는 회사 Agent ID·공개 상태·권한과 결합해 보여줄 목록을 만든다. GET /api/agents 응답을 그대로 임직원용 catalog로 공개하지 않는다. 에이전트 목록 구현도 함께 확인한다.

GET /api/sessions는 호출 사용자 전체 대화, 에이전트별 경로는 그 사용자와 해당 에이전트의 대화를 가져온다. UI에서 전체 내 대화와 에이전트별 대화를 모두 제공할 수 있다. 공개 권한이 회수된 에이전트의 과거 기록을 계속 읽게 할지는 사내 정책으로 정한다. 첫 구현에서는 목록·히스토리·호출 모두 현재 사용 권한을 요구한다.

세션 API는 대체로 data로 감싼 응답을 사용하며, 단일 세션 조회는 그 안에 다시 session과 events가 들어 있다. 히스토리용 /tasks 응답은 task 배열이다. 예를 들어 생성 응답의 ID는 data.id, 단일 조회의 ID는 data.session.id에서 읽는다. UI의 세션 함수가 이 차이를 처리하는 참고 구현이다.

백엔드 연동 모듈에서 이 envelope를 풀고, 프런트엔드의 api/ 모듈에서 이식한 컴포넌트가 기대하는 타입으로 맞춘다. task의 history·artifacts·도구 실행 정보를 단순한 문자열 배열로 줄이지 않는다. 기존 messageHandlers의 변환을 재사용해야 실시간 화면과 새로 연 히스토리가 같은 내용을 보여 준다.

Agent 생성·버전·공유 폼은 사내 backend 계약을 유지한다. Controller의 생성 API가 존재한다는 이유로 포털의 승인·배포 흐름을 통째로 중계하지 않는다. 리소스 생성은 기존 CRD 관리 경로와 연결한다.

예를 들어 김 직원과 이 직원이 같은 인사 도우미를 사용해도 각각 별도 세션을 만든다. kagent는 생성 시 UserID·AgentID를 저장하고 사용자 ID를 조건으로 세션을 조회한다. 에이전트를 공유하는 것과 대화를 공유하는 것은 별개다. 첫 적용에서는 에이전트 공유만 제공하고, 대화 공유 버튼과 X-Share-Token 전달은 별도 기능으로 남긴다.

사내의 원래 신원은 OIDC(OpenID Connect)의 (issuer, subject)로 식별한다. 이를 회사 내부의 안정적인 portalUserId에 매핑한다. 아래는 이 페이지에서 선택한 인증 연결안이다.

  1. 사내 백엔드가 기존 로그인 세션 또는 사용자 토큰을 검증해 portalUserId를 구한다.
  2. 에이전트·대화에 대한 권한을 검사한다.
  3. 백엔드가 내부용 JWT(JSON Web Token)를 만들어 sub에 portalUserId를 담고, Controller 요청의 Authorization: Bearer ...에 넣는다.
  4. kagent는 trusted-proxy 모드에서 그 식별자를 읽는다. 생성·조회·메시지 전송 모두 같은 ID를 사용한다.

Controller Helm values의 설정 발췌는 다음과 같다. 전체 설치 파일이 아니며, 기존 설치 값에 병합한다.

controller:
auth:
mode: trusted-proxy
userIdClaim: sub

이 모드는 ProxyAuthenticator가 JWT payload를 읽으며 서명 검증은 하지 않는다. 따라서 백엔드가 인증을 완료하고, Controller는 백엔드와 필요한 내부 runtime 통신만 허용하도록 네트워크 경계를 구성한다. Agent의 Controller callback까지 막지는 않는다. 새 oauth2-proxy를 반드시 추가해야 하는 설계는 아니다. 설정 이름은 고정 버전과 대조한다.

브라우저가 보낸 user_id, X-User-Id, X-Agent-Name을 그대로 전달하지 않는다. 백엔드가 서버에서 신원을 결정하고 허용한 헤더만 만든다. 인증 없는 기본 모드는 신원이 없으면 [email protected]를 사용하므로 직원별 기록을 구분하는 구성이 아니다. 기본 인증 구현

기존 대화를 이어 갈 때 검사할 것

섹션 제목: “기존 대화를 이어 갈 때 검사할 것”

sessionId를 안다고 접근을 허용하지 않는다. 백엔드는 호출 직원의 신원으로 세션을 조회하고, 그 세션의 agent_id를 회사의 배포 정보와 대조한 뒤 사용 권한을 검사한다. 브라우저가 다른 에이전트 이름이나 contextId를 넣어도 검사한 세션과 배포 위치로 덮어쓰거나 거부한다. 승인·재연결에 쓰는 taskId도 해당 세션의 task인지 확인한다.

새 대화는 현재 공개된 배포를 선택하지만, 기존 대화는 원래 연결된 배포로 보낸다. 대화 중에 공개 버전이 바뀌었다고 기존 세션을 자동으로 새 Agent에 연결하지 않는다. 여러 Controller·runtime을 지원하게 되면 conversationId → controllerRef + sessionId + deploymentId 매핑을 포털 DB에 추가할 수 있다. 이것은 대화 본문을 다시 저장하는 작업과 다르다.

아래 JSON은 백엔드가 Controller에 보내는 설명용 요청 예시다. hr-v1은 준비된 일반 Agent, 식별자는 설명용 값이며 실제 구현에서는 생성·조회 결과를 사용한다. 인증 헤더는 위 연결안대로 서버가 추가한다.

새 세션을 만들고 메시지 보내기

섹션 제목: “새 세션을 만들고 메시지 보내기”

직원이 인사 도우미를 고르면 백엔드는 회사 Agent ID를 kagent/hr-v1 배포로 해석하고 사용 권한을 확인한다. POST /api/sessions의 body는 다음과 같다. 직원 ID는 body로 받지 않는다.

{
"agent_ref": "kagent/hr-v1",
"name": "휴가 규정 문의"
}

정상 생성이면 201 응답의 data.id가 세션 ID다. 그 값을 채팅 URL에 넣고, 이후 A2A 메시지의 contextId로 사용한다. 원본 UI도 이 순서로 세션을 만들고 메시지를 보낸다. 세션 생성 코드

다음은 POST /api/a2a/kagent/hr-v1/에 보낼 body의 형태다.

{
"jsonrpc": "2.0",
"id": "request-uuid",
"method": "message/stream",
"params": {
"message": {
"kind": "message",
"messageId": "message-uuid",
"role": "user",
"contextId": "created-session-id",
"parts": [{ "kind": "text", "text": "휴가 신청 절차를 알려줘" }]
}
}
}

HTTP 요청은 Content-Type: application/json, 응답 요청은 Accept: text/event-stream을 사용한다. request ID·message ID·session ID·task ID를 같은 값으로 취급하지 않는다. 기본 형식은 A2A 클라이언트에 있고, 실제 runtime의 부가 metadata·확장은 이식한 ChatInterface의 요청 구성 로직도 함께 유지한다.

히스토리와 실행 상태 복원하기

섹션 제목: “히스토리와 실행 상태 복원하기”

채팅 페이지를 다시 열면 세션 정보를 조회하고 /tasks를 읽어 메시지·도구 결과를 복원한다. 완료된 task는 기록으로 표시하고, 실행 중인 task가 있으면 새 질문을 재전송하지 않고 A2A tasks/resubscribe로 stream을 다시 구독한다. 이 동작은 원본 A2A 클라이언트의 재연결 구현을 참고한다. 재연결은 새로운 실행을 만드는 작업이 아니며, 과거의 모든 stream 조각이 재전송된다는 보장으로 해석하지 않는다.

도구 승인이나 추가 질문으로 input-required 상태에 멈췄다면 해당 UI를 복원한다. 답변은 같은 contextId와 해당 taskId로 보내야 하므로 ToolCallGroup·AskUserDisplay와 응답 생성 로직을 함께 포팅한다. 채팅창에서 문장만 보내는 구현으로 줄이면 이 흐름을 잃는다. 공식 HITL 예제

사내 백엔드와 Next.js 중계는 SSE body를 조각 단위로 전달한다. 일반 JSON API의 response.json()이나 짧은 고정 timeout을 그대로 적용하지 않는다. 사용자별 히스토리는 공유 캐시에 넣지 않고, stream에는 버퍼링·자동 재시도를 적용하지 않는다. 연결이 끊겨도 모델이나 도구 실행이 끝났다고 단정하지 않는다.

특히 원본 ChatInterface의 중단 처리는 브라우저의 AbortController를 호출한다. 이것만으로 서버 task 취소나 이미 실행한 도구의 롤백이 보장되지는 않는다. 실행 취소 기능이 필요하면 사용 runtime의 A2A 취소 지원과 실제 종료 여부를 별도로 검증하고 화면 문구도 그 의미에 맞춘다. 중단 처리 소스

아래는 사내 적용 때 실행할 절차다. 이 문서를 작성하면서 실행한 통합 테스트 결과가 아니다. 첫 대상은 직원 두 명과 일반 Agent 하나로 잡는다.

  1. 버전과 준비물을 고정한다. kagent chart·Controller·Agent runtime과 가져올 UI를 v0.10.1 기준으로 맞춘다. 포털의 Next.js·React 호환성, 테스트 계정 두 개, Controller 주소, 사내 모델·MCP 연결을 확인한다.
  2. 백엔드 연결을 먼저 만든다. 검증한 사용자 신원으로 목록·세션 생성·조회·메시지 전송을 연결한다. 에이전트 사용 권한과 대화 소유권 검사를 이 경로에 둔다.
  3. 에이전트 하나의 채팅을 이식한다. 포털 메뉴 아래에 대화 목록과 채팅을 붙인다. 기존 로그인으로 조회와 stream이 모두 같은 백엔드를 거치는지 확인한다.
  4. 기록 복원과 상호작용을 검증한다. 새로고침·재연결·도구 승인·추가 질문을 시험한다. 이후 전체 내 대화 목록, 에이전트 선택, 생성·공유 화면 순으로 넓힌다.
  5. 기존 포털 배포에 포함한다. 기능 플래그나 제한된 사용자 그룹으로 먼저 공개한다. 되돌릴 때는 메뉴·기능을 비활성화하거나 이전 프런트엔드·백엔드 이미지를 배포하며, 대화 DB를 삭제하지 않는다.
시험 입력·행동기대 결과
김·이 직원이 같은 Agent에 각각 새 대화 생성서로 다른 session ID와 각자의 대화 목록
김 직원이 이 직원의 session ID로 조회·전송·이름 변경·삭제 시도백엔드에서 접근 거부, 내용·실행·변경 없음
Agent 사용 권한 회수 후 목록·직접 URL·메시지 요청정한 정책대로 목록과 직접 호출 모두 차단
새 대화를 만들고 새로고침·다른 메뉴 이동 후 복귀같은 세션의 메시지와 도구 결과가 복원됨
같은 사용자가 여러 Agent와 대화전체 내 대화에는 모두, 에이전트별 목록에는 해당 대화만 표시
응답 중 새로고침·네트워크 단절히스토리 조회 후 지원되는 실행은 재구독, 질문 자동 재전송 없음
승인 대기 중 새로고침 후 승인·거절같은 task의 대기 상태를 복원하고 중복 도구 실행 없이 이어짐
중단 버튼 클릭실제 확인한 범위대로 상태 표시, 연결 종료와 실행 취소를 혼동하지 않음
권한이 없는 agent ID·다른 context ID로 body 변조검사한 배포·세션과 다르면 거부, 다른 Agent 실행 없음

사용자별 목록과 SSE가 되는 것만으로 모든 Agent의 호환성을 보장하지 않는다. BYO Agent는 A2A 응답뿐 아니라 Controller의 세션 저장과 기록 조회까지 맞아야 하므로 별도 시험한다. SandboxAgent·AgentHarness와 MCP Apps도 일반 Agent 채팅을 검증한 뒤 해당 기능의 Context·API·권한을 추가한다.

포팅 이후에는 가져온 파일과 패치를 기록하고 업그레이드마다 이 표의 핵심 시나리오를 다시 확인한다. 1.x는 세션·UI 구조가 다르고 0.x DB의 직접 업그레이드 경로도 없으므로, 버전 숫자만 바꿔 연결하지 않는다. 공식 0.x 전환 안내