2026-05-16/Agent

새로고침 뒤에도 같은 에이전트 작업을 찾는 Runs API

Open WebUI와 Hermes Agent를 연결하면서 채팅 메시지, SSE 연결, 서버의 실행 작업을 서로 다른 상태로 다루게 된 이유

Open WebUI에서 에이전트에게 테스트 실패 원인을 찾아달라고 요청했다.

에이전트는 파일을 읽고, 테스트를 실행하고, 로그를 확인하기 시작했다. 작업이 예상보다 길어져서 브라우저를 새로고침했다.

채팅 화면은 다시 열렸다. 방금 시작한 작업은 어떻게 됐을까?

  • 서버에서 계속 실행 중일까?
  • 연결이 끊기면서 함께 취소됐을까?
  • 이미 끝났다면 결과를 다시 받을 수 있을까?
  • 다시 요청하면 같은 작업이 두 번 실행될까?

일반적인 채팅 응답에서는 자주 묻지 않는 질문이다. 하지만 도구를 사용하는 에이전트를 붙이는 순간 이 질문이 API 설계의 중심으로 들어왔다.

문제는 SSE가 아니었다.

브라우저의 연결과 서버의 작업을 같은 것으로 취급한 것이 문제였다.

채팅 메시지, 연결, 실행은 수명이 다르다

Open WebUI는 사용자가 메시지를 보내고 assistant 응답을 받는 채팅 UI다. OpenAI 호환 Chat Completions API와 연결하면 이 흐름이 자연스럽다.

GitHubGitHub - open-webui/open-webui: User-friendly AI Interface (Supports Ollama, OpenAI API, ...)User-friendly AI Interface (Supports Ollama, OpenAI API, ...) - open-webui/open-webuihttps://github.com/open-webui/open-webui
Rendering diagram...

짧은 답변을 생성하는 모델이라면 이 구조로 충분하다. 요청이 시작되고, 같은 HTTP 연결에서 토큰을 받다가, 최종 답변과 함께 끝난다.

Hermes Agent는 그 사이에 더 많은 일을 한다. 모델을 호출하고, 터미널이나 파일 도구를 실행하고, 결과를 다시 모델에 전달하며, 승인이나 외부 응답을 기다릴 수 있다.

GitHubGitHub - NousResearch/hermes-agent: The agent that grows with youThe agent that grows with you. Contribute to NousResearch/hermes-agent development by creating an account on GitHub.https://github.com/NousResearch/hermes-agent
Rendering diagram...

여기에는 서로 다른 세 가지가 있다.

message    사용자가 채팅창에 남긴 요청
connection 브라우저가 현재 이벤트를 받는 HTTP/SSE 연결
run        서버에서 도구를 사용하며 진행하는 작업

브라우저를 새로고침하면 connection은 끝난다. 그렇다고 message를 삭제할 이유는 없다. 몇 분 동안 실행되는 run도 마찬가지다.

세 상태를 하나로 취급하면 연결이 끊긴 뒤에 무엇을 복구해야 하는지 알 수 없다.

Chat Completions가 부족한 경우는 따로 있다

OpenAI 호환 API라는 이유만으로 에이전트에 부족한 것은 아니다.

현재 Hermes의 Chat Completions 스트림은 assistant 텍스트뿐 아니라 도구 진행 상황을 위한 hermes.tool.progress 이벤트도 보낼 수 있다. 한 번의 연결 안에서 도구 실행 상태를 보여주는 것이 목표라면, 별도의 실행 API가 없어도 된다.

구분 기준은 “도구를 쓰는가?”가 아니다.

다음 기능이 필요한지가 기준이다.

  • HTTP 연결이 끊겨도 같은 작업을 다시 찾는다.
  • 실행 중인지, 승인 대기인지, 실패했는지 별도로 조회한다.
  • 진행 중인 작업을 중지하거나 승인한다.
  • 마지막으로 받은 이벤트 이후부터 다시 따라간다.

이 기능에는 응답 스트림보다 오래 남는 식별자가 필요하다.

Chat Completions 요청에 임의의 ID를 붙이는 것만으로는 충분하지 않았다. 서버도 그 ID를 기준으로 실행 상태를 보관하고, 상태와 이벤트를 다시 제공해야 한다.

run_id가 연결과 작업을 분리한다

Hermes의 Runs API는 에이전트 작업을 run으로 만든다.

hermes-agent.nousresearch.comProgrammatic Integration | Hermes AgentThree protocols for driving hermes-agent from external programs: ACP, the TUI gateway JSON-RPC, and the OpenAI-compatible HTTP APIhttps://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration
POST /v1/runs
GET  /v1/runs/{run_id}
GET  /v1/runs/{run_id}/events
POST /v1/runs/{run_id}/approval
POST /v1/runs/{run_id}/stop

먼저 실행을 만든다.

POST /v1/runs
Content-Type: application/json

{
  "model": "hermes-agent",
  "input": "이 프로젝트에서 실패하는 테스트의 원인을 찾아줘"
}

서버는 run_id를 돌려준다.

{
  "run_id": "run_abc123",
  "status": "started"
}

이제 브라우저 연결이 아니라 run_abc123이 작업을 가리킨다.

Rendering diagram...

SSE 연결이 끊겼다면 같은 run의 상태를 조회하고 이벤트 스트림에 다시 연결할 수 있다. 작업을 중지하거나 승인을 전달할 때도 run_id를 사용한다.

Runs API가 연결을 끊기지 않게 만드는 것은 아니다. 연결이 끊겨도 작업을 다시 지칭할 수 있게 만든다.

이 차이가 중요했다.

이벤트 스트림에는 답변과 상태가 함께 흐른다

assistant의 답변은 문자열이다. 도구 실행과 승인 대기는 상태 변화다.

두 정보를 모두 일반 텍스트로 만들면 UI는 다음 문장을 구분하기 어렵다.

터미널을 실행하고 있습니다...

이 문장은 assistant의 최종 답변일까? 잠깐 보여줬다가 접어야 할 진행 상태일까? 대화 기록에 계속 남겨야 할까?

Runs API의 events endpoint는 토큰 delta, 도구 진행, 승인 대기, 완료와 실패 같은 수명주기 이벤트를 전달한다. UI는 이벤트 종류에 따라 표현을 달리할 수 있다.

텍스트 delta  → assistant 답변에 이어 붙인다
도구 시작     → 진행 상태를 표시한다
도구 완료     → 완료된 작업으로 접는다
승인 대기     → 승인 UI를 연다
run 실패      → 실행 오류로 표시한다
run 완료      → 스트림을 닫고 결과를 확정한다

실제 이벤트 이름과 payload는 Hermes 버전에 따라 확장될 수 있다. 그래서 Pipe가 문자열을 직접 검사하기보다 event type과 capability를 기준으로 처리하는 편이 안전하다.

Pipe는 프록시가 아니라 번역 계층이 됐다

Open WebUI의 Pipe Function은 사용자에게 하나의 모델처럼 보이면서, 내부에서는 임의의 Python 로직으로 외부 서비스를 호출할 수 있다.

docs.openwebui.comPipe Function / Open WebUIPipe Functions execute arbitrary Python code on your server. Function creation is restricted to administrators only. Only install from trusted sources and review code before importing. A malicious Function could access your file system, exfiltrate data, or compromise your entire system. For full details, see the Plugin Security Warning.https://docs.openwebui.com/features/extensibility/plugin/functions/pipe/

처음에는 Pipe를 얇은 프록시로 생각했다.

Open WebUI 요청 → Hermes 요청
Hermes 응답     → Open WebUI 응답

Runs API를 사용하자 역할이 달라졌다.

Open WebUI message  → Hermes run 생성 요청
Hermes text delta   → assistant 응답
Hermes tool event   → Open WebUI status event
Hermes run failure  → UI 오류
Hermes run_id       → message와 연결해 저장

Open WebUI가 Hermes의 실행 모델을 직접 알 필요는 없다. Hermes도 Open WebUI의 메시지 저장 방식을 알 필요가 없다. Pipe가 두 모델 사이를 번역한다.

번역 계층이 생긴 만큼 구현은 복잡해졌다. 그러나 도구 상태를 assistant 본문에 섞지 않고, 채팅 기록과 실행 상태를 각자 맞는 형태로 다룰 수 있게 됐다.

run_id를 메모리에만 저장하면 절반만 해결된다

처음 구현에서는 다음 매핑을 Pipe 프로세스의 메모리에 저장했다.

chat_id + message_id → hermes_run_id

이 방식으로 브라우저 새로고침과 같은 백엔드 프로세스 안의 재시도는 실험할 수 있었다.

하지만 Open WebUI 백엔드가 재시작되면 매핑이 사라진다. Hermes에는 run이 남아 있어도 어떤 채팅 메시지에서 시작됐는지 찾지 못한다.

재연결을 제품 기능으로 만들려면 적어도 다음 값이 대화 데이터와 함께 남아야 한다.

hermes_run_id
마지막으로 받은 event sequence
현재 run 상태

Open WebUI 메시지 metadata 같은 영속 저장소에 이 값을 기록하면 브라우저와 백엔드가 재시작된 뒤에도 복구할 기준이 생긴다.

여기에는 구현하지 못한 경계도 있다. Hermes는 종료된 run 상태를 무기한 보관하는 데이터베이스가 아니다. 공식 문서도 terminal state를 잠시 유지해 UI가 상태를 맞출 수 있다고 설명한다. 장기 실행 기록이 필요하다면 별도의 저장 정책이 필요하다.

run_id 하나를 얻었다고 내구성 있는 작업 큐가 완성되는 것은 아니다.

빨라진 것은 실행 시간이 아니라 첫 피드백이었다

Runs API로 옮긴 뒤 체감 응답은 빨라졌다.

모델 추론이나 도구 실행이 실제로 더 빨라진 것은 아니다. 사용자가 화면에서 처음 변화를 보는 시점이 앞당겨졌다.

Rendering diagram...

에이전트가 처음부터 터미널을 실행하면 assistant의 첫 문장은 늦게 올 수 있다. 그전에 run 시작과 도구 진행 상태를 보여주면 사용자는 멈춘 화면을 보고 기다리지 않아도 된다.

완료 시간은 그대로다. 아무 정보 없이 기다리는 시간이 줄었다.

실행 API가 필요한 때를 따로 구분한다

현재 Hermes에는 Chat Completions, Responses, Runs API가 함께 있다. 어느 하나가 항상 정답은 아니다.

짧은 assistant 응답이 필요하고 한 연결 안에서 도구 진행만 보여주면 된다면 OpenAI 호환 API가 가장 단순하다. Open WebUI도 이런 provider를 별도 Pipe 없이 연결할 수 있다.

Runs API의 비용은 다음 요구가 생길 때 감수할 만하다.

  • 작업이 HTTP 연결보다 오래 산다.
  • 새로고침 뒤에도 같은 작업을 다시 찾는다.
  • 도구 진행, 승인, 실패, 완료를 독립 상태로 다룬다.
  • 실행을 중지하거나 중간 지시를 전달한다.
  • 채팅 기록과 실행 기록을 별도로 보관한다.

Open WebUI는 대화 화면과 메시지 기록을 맡는다. Hermes는 도구를 사용하는 실행을 맡는다. Pipe는 message와 run을 연결하고, run event를 UI가 이해하는 형태로 바꾼다.

처음에는 OpenAI 호환 endpoint 하나를 등록하면 끝날 줄 알았다. 실제로 단순한 채팅이라면 그것으로 끝난다.

하지만 브라우저 연결이 끊긴 뒤에도 계속 추적해야 하는 작업은 채팅 응답과 수명이 다르다.

응답을 스트리밍하는 일과 실행을 관리하는 일을 분리한 이유다.

구현 예제와 남은 범위

이 글에서 사용한 Pipe의 전체 예제는 Gist에 남겨두었다.

GistOpen WebUI Pipe for Hermes Agent /v1/runsOpen WebUI Pipe for Hermes Agent /v1/runs. GitHub Gist: instantly share code, notes, and snippets.https://gist.github.com/clroot/7166a57ca3f6b5e55443a360aa8d3303

예제는 POST /v1/runs로 실행을 만들고, events endpoint를 읽어 텍스트와 status event로 나눈다. run_id 매핑은 프로세스 메모리에 저장하므로 Open WebUI 백엔드 재시작까지 복구하지는 못한다.

Open WebUI Function은 서버에서 임의의 Python 코드를 실행한다. 공식 문서의 안내처럼 코드를 검토한 뒤 관리자만 설치해야 하며, Hermes API Server를 외부에 열 때는 인증 키를 설정해야 한다.

설치 절차보다 먼저 확인할 것은 요구사항이다.

도구 진행 상황만 보여주면 되는지, 연결이 끊긴 뒤에도 같은 실행을 찾아야 하는지부터 구분해야 한다. 후자가 아니라면 더 단순한 OpenAI 호환 연결을 두고 굳이 실행 상태를 하나 더 만들 필요는 없다.