Building Hermes, Part 2: How I Safely Added a Planner Layer to Hermes

Jul 17 2026 · 4 min · Sieon

Hermes is the personal AI agent I run continuously in a Docker container on a VPS. Because it is already part of my daily workflow, replacing the runtime with a planner-based architecture in one big move was not realistic. If the conversation path breaks, it becomes an operational incident. If tool execution expands too quickly, the failure mode is even worse.

So the real work was not “make the planner smarter.” It was: how do I insert a new planning and execution layer into a live agent without breaking the old path? The rollout pattern was shadow-first, feature-gated, and evaluation-gated.

Why Hermes needed a planner layer

The previous Hermes loop was closer to a direct conversation runtime: receive the user request, reason inside the conversation loop, and call tools as needed. That is enough for simple tasks, but it becomes harder to control when the request has multiple dependent steps.

I wanted a separate structure for questions like:

  • what needs to be discovered before execution?
  • which actions are still too risky to run?
  • is there enough context to plan the next step?
  • which parts of the request should become a task graph?

The Planner Layer turns a request into task units, builds dependencies between them, and converts that graph into an execution plan. It also connects evidence from the Context Engine into the PlanningRequest. But the important constraint was that none of this moved directly into real execution on day one.

Adding it to a live agent safely

The first rule was simple: do not disturb the existing conversation path.

The planner hook sits right before the normal run_conversation(...) call in the CLI path. It only runs when an explicit environment flag is enabled. The default is off. If the planner crashes, produces a bad plan, or returns nothing useful, the original conversation path still runs.

The flags are layered:

  • Planner shadow execution
  • Read-only executor execution
  • Second Brain read-only capability execution

If the outer flag is off, the inner layers do nothing. That made it possible to expand authority one step at a time.

The first version only observed what would have happened. A Shadow Executor and Harness recorded the lifecycle without touching external systems. Only after that did I open a narrow read-only executor with a small allowlist of capabilities. Write operations, uploads, scheduling, and other outward-facing actions stayed closed.

I also separated the modules intentionally. Planning code lives under the planner layer. Shadow execution, read-only execution, policy, and adapters live under the execution layer. The code that decides what should happen and the code that can touch the outside world should not be collapsed into the same blob.

The Second Brain integration follows the same idea. Read paths and write-oriented ingestion paths are separated at the process and port level. The Planner and Executor are structurally unable to reach the upload path. That is better than relying on the code to “remember not to write.”

Using evaluations as a regression gate

Planner and Executor behavior can change because of a small heuristic. A single keyword can push the runtime toward the wrong capability. So I added a golden-case evaluation suite around the planner runtime.

The suite grew from roughly forty cases to roughly sixty cases. Planner and executor changes are compared against a baseline with deterministic metrics. Baseline updates are not automatic. If behavior changes, I review the diff first and only update the baseline when the change is intentional.

I also kept a known-failure case in the suite. One example: a request like “show my recent files” can lean toward read_file because of the word “file,” even though search_files is the better capability. Leaving that bug in the eval set makes it visible. It prevents the issue from becoming tribal knowledge that disappears after the next refactor.

The OOM incident was not caused by the planner

During the rollout, the VPS became slow shortly after planner work. The timing made the new code look suspicious. But the actual cause was a set of orphaned interactive CLI sessions. Each session had spawned its own MCP servers, and together they were consuming memory.

The lesson was basic but useful: an incident after a new code path does not prove the new code path caused it. Check the process table and memory state before trusting the timeline.

The fix was operational rather than architectural: idle session cleanup, a reaper cron, memory limits, and a shared MCP proxy. Reducing the runtime surface helped more than changing the planner itself.

What comes next

The current phase is focused on safely observing and validating read-oriented Planner and Executor behavior. Write execution, Scheduler integration, and Reflection wiring can come later, but they should follow the same rollout pattern: shadow path first, feature gates next, evaluation gates before broader authority.

Adding a planner layer to a running LLM agent is less like adding a feature and more like managing authority. The important question is not whether the planner can produce a convincing plan. The important question is whether the old path survives when the planner fails.


한국어 버전

Hermes는 내가 VPS의 Docker 컨테이너에서 상시 운영하는 개인 AI 에이전트다. 이미 매일 쓰는 시스템이라, 플래너 기반 구조로 한 번에 갈아엎는 방식은 선택하기 어려웠다. 대화 경로가 깨지면 바로 운영 장애가 되고, 도구 실행 권한이 잘못 넓어지면 더 위험한 문제가 된다.

그래서 이번 작업의 초점은 “더 똑똑한 플래너”보다 “이미 돌아가는 에이전트에 새 실행 레이어를 안전하게 심는 방법”이었다. 원칙은 shadow-first, 플래그 게이팅, 평가 게이팅이었다.

왜 Planner Layer가 필요했나

기존 Hermes는 사용자 요청을 받고 대화 루프 안에서 바로 판단해 도구를 호출하는 구조에 가까웠다. 단순 작업에는 충분하지만, 여러 단계가 얽힌 요청에서는 무엇을 먼저 알아야 하는지, 어떤 실행은 아직 위험한지, 근거가 충분한지를 별도 구조로 다루고 싶었다.

그래서 요청에서 작업을 뽑고, 의존성을 Task Graph로 만들고, 실행 계획으로 변환하는 Planner Layer를 붙였다. Context Engine의 근거도 PlanningRequest에 연결했다. 다만 처음부터 실제 실행으로 이어지게 하지는 않았다.

운영 중인 에이전트에 안전하게 넣기

첫 원칙은 기존 대화 경로를 건드리지 않는 것이었다. Planner 훅은 CLI의 대화 실행 직전에 붙였고, 별도 환경 플래그가 켜졌을 때만 shadow 경로가 동작하게 했다. 기본값은 꺼짐이다. 플래너가 예외를 내거나 이상한 계획을 만들어도 기존 대화는 그대로 진행되어야 했다.

플래그도 계층으로 나눴다.

  • Planner shadow 실행
  • Read-only executor 실행
  • Second Brain read-only capability 실행

바깥 플래그가 꺼져 있으면 안쪽 기능은 아무것도 하지 않는다. 처음에는 Shadow Executor와 Harness로 “실행했다면 무엇을 했을지”만 관찰했다. 그 다음에야 읽기 전용 capability 두 개를 허용목록으로 묶어 실제 실행을 열었다. 쓰기, 업로드, 스케줄링은 아직 열지 않았다.

모듈도 분리했다. 계획 관련 코드는 planner 영역에 두고, shadow executor, read-only executor, 정책, 어댑터는 execution 영역에 뒀다. 계획을 세우는 코드와 외부 세계를 건드리는 코드를 같은 덩어리로 두지 않기 위해서다.

Second Brain 연동에서도 같은 원칙을 적용했다. 읽기 API와 쓰기 전용 ingestion API를 프로세스와 포트 수준에서 분리했다. Planner와 Executor가 구조적으로 업로드 경로에 닿지 못하게 만든 것이다.

평가 프레임워크로 회귀를 막기

Planner와 Executor는 작은 휴리스틱 하나로도 선택이 바뀐다. 그래서 골든 케이스 기반 평가 프레임워크를 붙였다. 케이스 수는 40여 개에서 60여 개로 늘렸고, planner나 executor 변경 전후로 베이스라인과 비교했다.

베이스라인 갱신은 자동화하지 않았다. 결과가 바뀌면 먼저 사람이 보고, 의도한 변화일 때만 명시적 명령으로 갱신한다. known-failure도 하나 남겼다. 예를 들어 “recent files를 보여달라”는 요청이 파일 힌트 때문에 검색이 아니라 읽기 쪽으로 기우는 버그다. 고치지 못한 결함도 스위트 안에 박아두면 조용히 잊히지는 않는다.

OOM 사고에서 배운 것

작업 중 VPS가 느려진 적이 있었다. 시점만 보면 새 Planner 코드가 원인처럼 보였다. 하지만 실제 원인은 고아가 된 인터랙티브 CLI 세션들이었다. 각 세션이 MCP 서버들을 따로 띄우면서 메모리를 잡아먹고 있었다.

교훈은 단순했다. 새 코드 배포 직후 장애가 났다고 해서 반드시 새 코드가 범인은 아니다. 직감은 출발점일 뿐이고, 프로세스와 메모리 상태를 먼저 확인해야 한다. 이후 idle 세션 자동 종료, reaper cron, 메모리 제한, 공유 MCP proxy 같은 운영 장치를 추가했다.

다음 단계

현재 단계는 읽기 중심 Planner/Executor를 안전하게 관찰하고 검증하는 데 초점이 있다. 다음으로 write 실행, Scheduler, Reflection 같은 레이어를 붙일 수 있지만 아직 미착수다. 바로 여는 대신, 이번과 같은 방식으로 shadow 경로와 플래그 계층, 평가 게이팅을 먼저 준비할 생각이다.

운영 중인 LLM 에이전트에 새 실행 레이어를 넣는 일은 기능 추가라기보다 권한을 관리하는 일에 가깝다. 잘 만든 플래너보다 중요한 것은 실패했을 때 기존 경로가 안전하게 살아남는 구조였다.

References

  1. Hermes Agent Documentation