Building Hermes, Part 3: Adding a Planner Layer to a Live LLM Agent Without Breaking It

Jul 17 2026 · 4 min · Sieon

Hermes is my personal AI agent running continuously in a Docker container on a VPS. It is not a toy script. It is a system I actually talk to, a system that calls tools, writes posts, and helps me operate real workflows. That meant I could not simply replace the existing execution path just because I wanted to add a Planner Layer.

The reason for adding a planner was clear. The existing agent received a user request and moved directly into the model and tool-calling flow. That was good enough for short tasks, but longer requests exposed a different need. The agent needed to reason more explicitly about what should happen first, what evidence was missing, and which steps were executable versus only conceptual. The goal was to let the agent build an internal task graph, validate an execution plan, and only then pass safe work downstream.

The most important principle in this work was shadow-first. I inserted the Planner in front of the existing conversation path, but kept it disabled by default. The entry point was a thin hook immediately before conversation execution. Only when HERMES_PLANNER_SHADOW was enabled would the Planner observe the request. When the flag was off, the existing run_conversation path behaved exactly as before. If the Planner failed, the user conversation would not break.

After that, I added the Task Extractor, Task Graph, and Execution Plan conversion stages one by one. I did not start by executing anything. I focused first on recording how a request would be decomposed if it were going to run. Then I added the Shadow Executor and Harness so I could observe the execution lifecycle without making real tool calls. That step mattered because I needed to see how live requests were being interpreted before I cared too much about whether the code looked neat.

I also avoided opening execution privileges all at once. The flags are layered. The outer flag enables Planner shadow mode. Inside that, another flag enables the read-only executor. Inside that, another flag enables the Second Brain read executor. If the outer flag is off, none of the inner capabilities do anything. This makes rollout gradual and gives me a clear switch to turn off if a specific layer misbehaves.

When I started enabling read execution, I kept the allowlist intentionally small. At first, only two read-only capabilities were allowed to execute for real. More importantly, I separated the structure around those permissions. The Second Brain path has read APIs and write APIs split at the process and port level. The Planner and Executor can reach the read path, but they are structurally unable to reach upload or write logic. I did not want write safety to depend only on coding conventions. I wanted the deployment shape itself to enforce the boundary.

I added the evaluation framework at the same time to prevent regressions. The Planner and Executor may look like small pieces of plumbing, but in practice they influence which tools the agent chooses. So I built golden cases in JSONL, added a deterministic runner, and attached metrics. Before and after changes, I can run the evaluation suite. Baseline updates require an explicit command. The suite grew from 42 cases to 62 cases during this work.

One useful detail is that I intentionally left one known-failure case in the suite. For example, a request like “show me recent files” was being misclassified as a read operation instead of a search operation because of a file hint. Rather than leaving that unfixed behavior outside the test suite, I made it visible as something to track. For an operating system, visible failure is better than hidden failure.

There was also an OOM incident in the middle of the work. Right after the Planner changes, the VPS became sluggish, so my first instinct was to suspect the new code. The actual cause was different: orphaned interactive CLI sessions had been left behind. Each session had launched multiple MCP servers, and the accumulated processes were consuming memory. The fix was not a Planner rollback. It was a reaper cron job, memory limits, a shared MCP proxy, and idle session cleanup. The lesson was simple. A failure after a deployment is not always caused by the code that just shipped. Intuition is only the starting point. Instrumentation has to confirm the cause.

At this stage, the result is a Planner Layer inside a live agent that can observe, validate, and perform limited read-only execution without breaking the existing path. Write execution is still closed. Higher-level loops like Scheduler and Reflection are not part of this stage yet. The current work is about giving the agent a safer way to understand requests, bundle the evidence it needs, and execute only tightly scoped read operations.

When you operate an LLM agent in the real world, the riskiest moment is not always when you add a new feature. It is when that feature quietly inherits every permission the old path already had. So I added this one slowly. First shadow mode, then layered flags, then evaluation, then read-only permissions. It is not the flashiest architecture, but for adding a Planner to an agent that is already running, this amount of caution feels right.

한국어 버전

운영 중인 LLM 에이전트에 Planner Layer를 안전하게 넣는 법

Hermes는 내가 VPS 위 Docker 컨테이너에서 상시 운영하는 개인 AI 에이전트다. 단순한 실험용 스크립트가 아니라, 실제로 대화하고 도구를 호출하고 글을 쓰고 작업을 돕는 시스템이다. 그래서 Planner Layer를 넣고 싶다고 해서 기존 실행 경로를 한 번에 갈아엎을 수는 없었다.

Planner가 필요한 이유는 분명했다. 기존 에이전트는 사용자의 요청을 받아 바로 모델과 도구 호출 흐름으로 들어갔다. 짧은 작업에는 충분했지만, 여러 단계가 섞인 요청에서는 “무엇을 먼저 해야 하는지”, “어떤 근거가 부족한지”, “실행 가능한 단계와 아닌 단계를 어떻게 나눌지”를 더 명시적으로 다룰 필요가 있었다. 결국 목표는 에이전트가 바로 행동하기 전에, 내부적으로 작업 그래프를 만들고 실행 계획을 검증한 뒤 안전하게 내려보내는 것이었다.

이번 작업에서 가장 중요하게 둔 원칙은 shadow-first였다. Planner를 기존 대화 경로 앞에 바로 끼워 넣되, 기본값은 꺼진 상태로 두었다. 진입점은 대화 실행 직전의 얇은 훅이었다. HERMES_PLANNER_SHADOW가 켜졌을 때만 Planner가 요청을 관찰하고, 꺼져 있으면 기존 run_conversation 경로는 그대로 동작한다. Planner가 실패해도 사용자 대화가 깨지지 않는 구조를 먼저 만든 셈이다.

그 다음에는 Task Extractor, Task Graph, Execution Plan 변환 단계를 차례로 붙였다. 처음부터 실행하지 않고, “이 요청을 실행한다면 어떤 태스크로 나눌 것인가”를 기록하는 데 집중했다. 이후 Shadow Executor와 Harness를 추가해서 실제 도구 호출 없이 실행 라이프사이클을 관찰했다. 이 단계가 꽤 중요했다. 코드가 맞는지보다 먼저, 운영 중인 요청들이 어떤 계획으로 해석되는지 볼 수 있었기 때문이다.

실제 실행 권한도 한 번에 열지 않았다. 플래그를 계층으로 나눴다. 바깥에는 Planner shadow 플래그가 있고, 그 안쪽에 read-only executor 플래그, 다시 그 안쪽에 Second Brain 읽기 executor 플래그가 있다. 바깥 플래그가 꺼져 있으면 안쪽 기능은 아무것도 하지 않는다. 이렇게 해두면 기능을 단계적으로 켤 수 있고, 문제가 생겼을 때 어느 층을 끄면 되는지도 분명해진다.

읽기 실행을 시작할 때도 허용목록을 작게 잡았다. 처음에는 read-only capability 두 개만 실제 실행하게 했다. 더 중요한 것은 구조적 격리였다. Second Brain 쪽도 읽기 API와 쓰기 API를 프로세스와 포트 수준에서 분리했다. Planner와 Executor가 읽기 경로에는 접근할 수 있지만, 업로드나 쓰기 로직에는 구조적으로 닿지 못하게 했다. 권한 체크를 코드 관례에만 맡기지 않고, 배치 구조 자체로 막고 싶었다.

회귀를 막기 위해 평가 프레임워크도 같이 넣었다. Planner와 Executor는 겉으로 보기에는 작은 변경처럼 보여도, 실제로는 “어떤 도구를 선택하는가”에 영향을 준다. 그래서 골든 케이스를 JSONL로 쌓고, 결정적 러너와 메트릭을 붙였다. 변경 전후에는 평가를 돌리고, 베이스라인 갱신은 명시적인 커맨드로만 하게 했다. 케이스 수는 42개에서 62개로 늘어났다.

재미있는 점은 일부러 known-failure 케이스도 하나 남겨두었다는 것이다. 예를 들어 “최근 파일을 보여줘” 같은 요청이 file 힌트 때문에 검색이 아니라 읽기 쪽으로 잘못 분류되는 문제가 있었다. 고치지 못한 버그를 테스트 밖에 방치하는 대신, 스위트 안에 박아두고 회귀 추적 대상으로 만들었다. 실패를 숨기지 않는 편이 운영에는 더 낫다.

중간에 OOM 사고도 있었다. Planner 작업 직후 VPS가 느려졌기 때문에 처음에는 새 코드가 원인이라고 의심했다. 하지만 실제 원인은 고아가 된 인터랙티브 CLI 세션들이었다. 각 세션이 여러 MCP 서버를 띄운 채 남아 있었고, 그 누적이 메모리를 잡아먹고 있었다. 해결은 Planner 코드 롤백이 아니라 reaper cron, 메모리 제한, 공유 MCP proxy, idle 자동 종료였다. 이 사건에서 배운 것은 단순하다. 새 코드 배포 직후의 장애가 항상 새 코드 때문은 아니다. 직감은 출발점일 뿐이고, 원인은 계측으로 확인해야 한다.

이번 단계까지의 결과는 “운영 중인 에이전트 안에 Planner Layer를 넣되, 기존 경로를 깨지 않고 관찰, 검증, 제한된 읽기 실행까지 확장한 것”이다. 아직 write 실행은 열지 않았다. Scheduler나 Reflection 같은 상위 루프도 미착수 상태다. 지금은 Planner가 요청을 더 잘 이해하고, 필요한 근거를 묶고, 안전한 read-only 실행을 할 수 있는 기반을 만든 정도다.

LLM 에이전트를 실제로 운영하다 보면 가장 위험한 순간은 새 기능을 넣을 때가 아니라, 새 기능이 기존 습관대로 조용히 모든 권한을 갖게 될 때라고 생각한다. 그래서 이번에는 느리게 넣었다. shadow로 먼저 보고, 플래그로 층을 나누고, 평가로 막고, 권한은 읽기부터 열었다. 화려한 구조는 아니지만, 이미 돌아가고 있는 에이전트에 Planner를 심는 방식으로는 이 정도의 보수성이 맞다고 느꼈다.