TL;DR — LLM 에이전트의 판단 자체를 결정론적으로 통제하기는 어렵지만, 어떤 맥락을 전달하고 어떤 산출물을 요구할지는 외부에서 관리할 수 있습니다. btwin orchestration은 무거운 agent framework보다 작은 control plane을 두고, thread / protocol / phase / guard / hook으로 작업 흐름을 검증하는 실험입니다.
Table of contents
Open Table of contents
들어가며
LLM에게 여러 작업을 맡기다 보면 어느 순간 “작업을 시작했습니다”라는 응답 뒤에 실제 진행이 멈추는 상황을 만납니다. 이 글은 그 문제를 해결하기 위해 btwin에서 시도한 lightweight orchestration 구조를 정리합니다.
이 글에서 다루는 내용:
- LLM 에이전트 작업이 멈추는 이유에 대한 관찰
- 무거운 agent framework 대신 control plane을 둔 이유
- thread / protocol / phase / guard / gate 모델
- Codex hook 기반 completion contract
- helper workspace와 instruction layer 분리
사전 지식: Codex, Claude Code 같은 CLI 기반 LLM 도구와 hook / MCP의 기본 개념을 전제로 합니다.
1. 문제: LLM은 계속 실행되는 프로세스가 아닙니다
LLM은 스스로 계속 실행되는 worker process가 아닙니다. 일반적인 사용 흐름에서는 사용자가 입력을 보내거나 외부에서 dispatch를 넣을 때 다음 행동을 시작합니다.
따라서 LLM의 내부 판단을 완전히 결정론적으로 통제하기는 어렵습니다. 대신 외부에서 더 안정적으로 관리할 수 있는 부분이 있습니다.
- 이번 turn에 어떤 맥락을 줄 것인가
- 현재 작업 단계는 무엇인가
- 어떤 output을 남겨야 완료로 인정할 것인가
- 조건을 만족하지 못했을 때 어떻게 block하고 다시 안내할 것인가
btwin orchestration은 이 지점에 집중했습니다. 에이전트의 사고를 프레임워크가 강하게 통제하는 방식이 아니라, LLM이 일하기 좋은 작은 작업 레일을 제공하는 방식입니다.
2. 설계 기준: 작은 control plane을 둡니다
btwin orchestration의 기준은 다음과 같았습니다.
- 전체 작업 맥락을 거대한 prompt로 계속 주입하지 않습니다.
- LLM 에이전트가 사용할 수 있는 control plane을 MCP, CLI, API로 제공합니다.
- 작업 흐름은 사람이 읽을 수 있는 thread state로 남깁니다.
- 완료 여부는 에이전트의 말이 아니라 필요한 contribution을 기준으로 봅니다.
전체 구조는 다음과 같습니다.
flowchart TD
U["User / Foreground Agent"] --> CP["btwin Control Plane"]
CP --> AR["Dispatched Agent Runtime"]
AR --> F["Agent Output / Contribution"]
F --> R["btwin Record / Next Action"]
| 계층 | 내부 구성 |
|---|---|
| btwin Control Plane | Memory/Context Store, Thread State, Protocol/Phase State, Guard/Gate Rules |
| Dispatched Agent Runtime | Common Instructions, Agent Identity/Role Contract, Dynamic Turn Brief |
3. 모델: Thread, Protocol, Phase, Guard, Gate
btwin은 협업 흐름을 Thread, Protocol, Phase, Guard, Gate로 나눠 관리합니다.
| 개념 | 역할 |
|---|---|
| Thread | 하나의 작업 흐름입니다. 참여 agent, 현재 phase, contribution, 다음 행동 상태를 보관합니다. |
| Protocol | thread가 어떤 절차로 진행될지 정의하는 협업 규칙입니다. |
| Phase | 현재 해야 할 작업 단위입니다. 예를 들어 context, discussion, review, decision이 있습니다. |
| Guard | 현재 phase에서 이 행동이 허용되는지 확인하는 runtime 안전장치입니다. |
| Gate | phase 결과를 보고 다음 상태를 결정하는 전이 규칙입니다. |
개념적으로는 다음과 같습니다.
phase = 지금 어떤 작업을 하는가
guard = 이 행동이 현재 상태에서 허용되는가
gate = 이번 phase 결과를 보고 어디로 이동할 것인가
현재 구현에서 gate는 별도 독립 객체라기보다 from / on / to 형태의 transition을 gate 개념으로 해석합니다. 중요한 점은 LLM이 “다음 단계로 넘어가도 된다”고 말하는 것과 실제 thread state가 전이되는 것을 분리했다는 점입니다.
4. Hook: 완료 조건을 외부에서 확인합니다
LLM의 작업 품질은 비결정적일 수 있지만, 완료를 인정하기 위한 최소 산출물은 외부에서 확인할 수 있습니다. btwin은 Codex hook을 이용해 turn 종료 시점에 completion contract를 검사합니다.
flowchart LR
A["Agent Turn"] --> B["Codex Hook"]
B --> C["btwin Workflow Check"]
| Hook | 역할 |
|---|---|
SessionStart | thread / phase context 복원 |
UserPromptSubmit | 현재 phase contract overlay |
Stop | 종료 직전 산출물 검증 |
| Workflow Check | 내용 |
|---|---|
| required output | 존재 여부 확인 |
| actor / phase / contribution | 조건 확인 |
| guard | 위반 시 Stop block |
Stop hook은 provider turn이 끝나려는 시점에 실행되는 마지막 검문소로 사용했습니다. 현재 phase에서 필요한 contribution이 없으면 hook 응답으로 decision: block을 반환해 turn 종료를 막고, 에이전트에게 어떤 산출물을 남겨야 하는지 다시 안내합니다.
phase transition이나 next action 계산은 hook이 직접 처리하지 않습니다. hook은 attempt, check, blocked 같은 workflow event를 남기고, 이후 protocol / delegation layer가 다음 행동을 계산합니다.
5. Instruction Layer: 역할과 작업 지시를 분리합니다
에이전트별 역할 지침도 중요한 설계 포인트였습니다. 단순히 CLI를 한 번 실행한 뒤 첫 turn을 사용해 “너는 reviewer입니다”라고 말하면, 역할 주입 자체가 작업 turn을 소비합니다.
btwin은 Codex가 startup 시점에 현재 작업 경로 기준으로 instruction, config, hook layer를 읽는다는 점을 활용했습니다. target repo 내부에 helper workspace를 만들고, 이 경로를 dispatched agent의 cwd로 사용합니다.
| 레이어 | 내용 |
|---|---|
| 1. Global Layer | global config, user MCP / hooks |
| 2. Project Layer | project root, repo AGENTS.md, project .codex config |
| 3. btwin Helper Overlay | .btwin/helpers/<agent>/ workspace, AGENTS.md, .codex/hooks.json |
| 4. Runtime Binding | thread_id, agent_name, launch developer instructions |
| 5. Dynamic Turn Brief | current phase, required action, expected output |
flowchart TD
A["1. Global Layer"] --> B["2. Project Layer"]
B --> C["3. btwin Helper Overlay"]
C --> D["4. Runtime Binding"]
D --> E["5. Dynamic Turn Brief"]
이 구조에서 기존 repo의 AGENTS.md나 .codex 설정은 덮어쓰지 않습니다. repo-local helper overlay 아래에 별도 config를 두고, agent identity는 launch developer instructions로, phase별 작업 지시는 dispatch message로 전달합니다.
helper overlay는 target repo 내부에 있고, Codex가 해당 프로젝트를 trusted project로 인식할 때 가장 안정적으로 동작합니다. btwin도 helper overlay를 만들기 전에 workspace가 Git repo 내부인지, Codex 설정에서 trusted project인지 확인하도록 설계했습니다.
6. Delegation: 멈춘 작업을 사람이 다시 잡을 수 있게 합니다
orchestration에서 중요한 것은 에이전트가 항상 성공하는 구조를 만드는 것이 아닙니다. 실패하거나 멈췄을 때 사람이 어디서 이어가야 하는지 알 수 있어야 합니다.
btwin은 delegation을 start / status / wait / respond / stop 같은 bounded loop로 관리합니다. helper가 멈추거나 추가 입력이 필요할 때 다음과 같은 정보를 사람이 볼 수 있게 합니다.
required_actionexpected_outputreason_blockedsuggested_next_commandtarget_roleresolved_agent
이 정보는 단순한 로그보다 중요합니다. 현재 상태를 사람이 다시 해석하지 않아도 다음 행동을 결정할 수 있기 때문입니다.
정리하며
핵심 요약:
- LLM의 판단 자체를 결정론적으로 통제하기보다, 작업 단계와 산출물 조건을 외부에서 관리했습니다.
Thread / Protocol / Phase / Guard / Gate는 에이전트 작업을 사람이 이해 가능한 상태로 나누기 위한 모델입니다.- Codex hook은 “끝났습니다”라는 응답을 그대로 믿지 않고, 필요한 contribution이 남았는지 확인하는 검문소로 사용했습니다.
- helper overlay와 launch instruction을 분리해 기존 작업 환경을 오염시키지 않고 역할과 작업 지시를 얹었습니다.
참고 자료
- btwin GitHub — btwin 프로젝트 저장소
- OpenAI Codex: AGENTS.md — Codex instruction loading 관련 문서
- OpenAI Codex: Hooks — Codex hook 동작 방식
- OpenAI Codex: MCP — Codex에서 MCP tool을 연결하는 방식
- OpenAI Codex: App Server — Codex app-server 관련 문서
- Claude Code CLI reference — Claude Code CLI reference
이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.