도구를 호출할 수 있는 Agent Core는 복잡하지 않습니다. 최소 구현은 메시지를 관리하고, 모델에 요청을 보내 Tool Call을 실행한 다음, Tool Result를 다음 요청에 다시 넣으면 됩니다.
실제로 Agent Framework을 빠르게 비대하게 만드는 부분은 대개 Loop 밖에서 발생합니다.
Coding Agent는 곧 파일 시스템 도구, Shell, MCP, Skill, 권한 제어, 모델 전환, Session, Compaction, Command, UI, Telemetry, SubAgent를 갖게 됩니다. 기능 하나를 추가할 때마다 구현 선택지는 두 가지입니다. Agent Core를 계속 수정하거나, 외부 모듈이 연결될 수 있는 안정적인 확장 인터페이스를 제공하거나.
Pi는 후자를 선택했습니다. 비교적 한곳에 모인 Agent Runtime을 유지하는 동시에 Extension을 통해 많은 제품 기능을 핵심 밖으로 분리했습니다.
이 글의 초점은 Pi가 어떤 플러그인 API를 제공하는지가 아니라 더 근본적인 프레임워크 설계 문제입니다. 어떤 기능은 Agent Core에 들어가야 하고, 어떤 기능은 Harness 확장으로 제공되어야 하는가입니다.
Tool Loop에서 계속 커지는 Core로
1부에서는 이미 비교적 안정적인 Agent Loop의 경계를 확보했습니다.
Context
↓
Model
↓
Tool
↓
Input Delivery
↓
Stop Condition
이 구조는 한 번의 Agent 작업이 어떻게 진행되는지는 설명할 수 있지만, 완성된 제품에 필요한 모든 기능을 담지는 못합니다.
권한 제어를 예로 들면, 가장 직접적인 구현은 각 Tool을 실행하기 전에 판단을 추가하는 것입니다.
이어서 감사 기록도 필요합니다.
그다음에는 Hook을 추가합니다.
그다음에는 원격 실행을 추가합니다.
모든 기능을 이 방식으로 메인 실행 함수에 넣는다면, Agent Loop는 점진적으로 전체 시스템의 통합 거점이 됩니다. 어떤 기능이든 이를 수정할 수 있고, 어떤 수정이든 다른 기능에 영향을 줄 수 있습니다.
여기서 Harness의 첫 번째 역할이 드러납니다.
Core는 안정적인 실행 의미를 정의하고, Harness는 외부 기능을 그 의미의 경계에 맞춰 구성합니다.
따라서 확장 시스템의 가치는 ‘제3자가 플러그인을 작성할 수 있도록 하는 것’이 아니라 Core의 증가 속도를 제어하는 데 있습니다.
Pi의 Extension 설계: 안정적인 Core + 확장 진입점
Pi는 자신을 minimal terminal coding harness로 포지셔닝합니다. 기본 Coding Agent는 소량의 기초 Tool만 제공하고, 더 많은 동작은 Extensions, Skills, Prompt Templates, Packages를 통해 추가됩니다.[1][2]
Extension은 Tool을 등록할 수 있습니다.
라이프사이클 이벤트를 감시할 수도 있습니다.
Command, Shortcut, Provider를 등록하거나, 실행 중인 Session에 메시지를 주입할 수도 있습니다.[1][3]
이 API들은 겉보기에는 차이가 커 보이지만, 실제로는 두 방향으로 묶을 수 있습니다.
첫 번째 방향은 Contribution입니다.
Extension
↓
向 Runtime 注册一项能力
Tool
Command
Provider
Renderer
...
두 번째 방향은 Hook / Interception입니다.
Runtime lifecycle
↓
Extension observes / modifies / blocks
전자는 ‘시스템에 무엇이 존재하는지’를, 후자는 ‘시스템이 특정 경계에 도달했을 때 무엇이 일어나는지’를 확장합니다.
따라서 Pi Extension의 핵심 구조는 다음과 같이 추상화할 수 있습니다.
이 모델은 ToolPlugin, CommandPlugin, HookPlugin을 각각 따로 설계하는 것보다 통일된 개발 모델을 만들기가 쉽습니다. Extension 자체는 클로저 상태를 가지고 있으며, 같은 Extension에 속한 Tool, Hook, Command는 상태를 직접 공유할 수 있습니다.
Pi는 한때 Hook과 사용자 정의 Tool을 분리했다가 나중에 Extension으로 통합했습니다. 이 변화 역시 같은 방향을 보여줍니다. 확장 단위는 등록 유형이 아니라 ‘하나의 기능 모듈’을 중심으로 구성해야 한다는 것입니다.[4]
ExtensionAPI는 경계이지, 컨테이너 자체가 아니다
플러그인 아키텍처에서는 흔히 다음과 같은 오해를 범합니다. 플러그인이 ‘무엇이든 할 수 있도록’ Application이나 내부 객체를 통째로 노출한다는 것입니다.
단기적으로는 매우 유연하지만, 장기적으로는 사실상의 내부 API가 됩니다. 플러그인은 어떤 구현 세부사항에도 접근할 수 있고, Core는 리팩터링할 여지를 잃습니다.
Pi는 ExtensionAPI를 플러그인 진입점으로 삼습니다. 확장은 이를 통해 기능을 등록하고, 메시지를 보내고, 정해진 범위의 상태를 읽거나 수정합니다.[1][3]
이 관계는 다음과 같이 이해할 수 있습니다.
Extension
│
▼
ExtensionAPI
│
▼
Runtime / Session / UI / Registry
API의 의미는 플러그인이 어떤 시스템 경계에 영향을 미칠 수 있는지 규정하는 데 있습니다.
예를 들어 다음과 같습니다.
registerTool()
registerTool()을 사용하면 Extension이 모델이 호출할 수 있는 기능을 추가할 수 있습니다.
on('tool_call')
on('tool_call')을 사용하면 Extension이 Tool 호출 경계에서 제어에 참여할 수 있습니다.
sendMessage()
sendMessage()를 사용하면 Extension이 새로운 입력을 Agent Runtime에 전달할 수 있습니다.
이 기능들은 강력하지만, 호출은 여전히 Harness가 정한 진입점을 거칩니다. Core 내부의 상태 구조는 계속 진화할 수 있습니다.
따라서 확장 API에서 더 중요한 것은 메서드의 개수가 아니라, 기능 경계가 안정적인지 여부입니다.
Hook을 Observer만으로 이해하면 안 되는 이유
pi.on(...)은 Event Bus로 쉽게 이해되지만, Pi의 일부 Hook은 실행 동작을 수정하거나 심지어 막을 수도 있습니다.
예를 들어 Tool Call Hook은 호출을 검사하거나 수정하거나 거부할 수 있고, Context Hook은 모델이 곧 보게 될 메시지를 조정할 수 있습니다.[1]
이런 Hook은 Middleware / Interceptor에 더 가깝습니다.
Runtime operation
↓
Extension A
↓
Extension B
↓
Core behavior
Observer의 기본 의미는 이미 일어난 사실을 관찰하는 것입니다.
event happened
↓
listener notified
반면 Interceptor는 동작 체인 내부에 놓입니다.
operation requested
↓
interceptor
↓
modify / block / continue
이 차이는 플러그인 시스템의 복잡도에 직접 영향을 줍니다.
순수 Observer는 알림과 예외 격리만 고려하면 됩니다. Interceptor는 순서, 조기 종료, 오류 전파, 비동기 동작, 여러 확장이 동시에 결과를 수정할 때의 조합 의미까지 추가로 정의해야 합니다.
Pi의 현재 tool_call은 이를 다음과 같이 명시합니다. 앞선 Handler가 입력에 가한 변경은 뒤의 Handler에게도 보이며, Handler는 반환값을 통해 호출을 막을 수 있습니다. 이벤트는 단순한 브로드캐스트가 아니라 실행 프로토콜의 일부입니다.[1]
이것은 Harness 설계가 통제해야 할 또 다른 경계입니다. 모든 이벤트에 수정 권한을 부여할 필요는 없습니다.
관측만 필요로 하는 확장 지점은 읽기 전용으로 유지하고, 정책 개입이 실제로 필요한 실행 경계에만 Interception을 제공해야 합니다.
동적 등록은 런타임 확장을 해결하지만, 라이프사이클은 여전히 별개의 문제
Pi Extension은 실행 중에 Tool을 등록할 수 있습니다. 등록이 끝나면 새 Tool은 현재 Session의 Tool 집합에 들어갈 수 있습니다.
사용자 관점에서는 이미 동적 플러그인 시스템의 중요한 기능을 갖췄습니다.
Runtime
│
├── Tool A
├── Tool B
│
└── Extension loads
↓
Tool C
하지만 동적으로 ‘추가’하는 기능은 문제의 절반일 뿐입니다.
더 복잡한 문제는 기능이 시스템에서 빠져나갈 때 발생합니다.
어떤 Extension이 다음과 같은 작업을 한다고 가정해 봅시다.
register Tool A
register Command B
subscribe tool_call
start timer
provide service
플러그인을 언로드할 때 시스템은 이러한 변경을 모두 되돌려야 합니다.
의존성을 계속 추가하면 문제가 더 커집니다.
Plugin A
└── provides Service X
Plugin B
├── depends on Service X
└── provides Service Y
Plugin C
└── depends on Service Y
A를 언로드하면 B는 계속 실행해야 하나요? B가 제공하는 Y는 여전히 유효한가요? C는 어떻게 처리해야 하나요? A를 다시 로드하면 B와 C를 다시 시작해야 하나요?
이런 질문은 Extension Registry 자체를 넘어 동적 조합과 라이프사이클 관리 영역으로 들어갑니다.
Pi의 설계는 주로 Extension을 비교적 안정적인 Runtime 주변에 구축하는 데 초점을 둡니다. 다음 형태를 표현하기에 적합합니다.
stable runtime
+
dynamic contributions
+
lifecycle hooks
DeepSeek Harness는 더 급진적인 구조를 택합니다. Agent Loop, Session, Tool Registry, LLM Adapter 같은 기능 자체도 플러그인이 제공합니다.[5] 그렇다면 플러그인 시스템은 Runtime 구조 자체가 동적으로 바뀌는 것까지 떠안아야 합니다.
이야말로 Cordis가 DeepSeek Harness에 들어맞는 지점입니다.
Core의 경계를 판단하는 한 가지 방법
어떤 기능이 Agent Core에 들어가야 하는지는 세 가지 질문으로 판단할 수 있습니다.
먼저, 그 기능이 모든 Agent Step의 기본 의미에 참여하는지 확인합니다.
예를 들어 Context 구성, Model Request, Tool Result의 되돌림, 중지 조건은 Loop의 기본 실행 구조에 해당합니다. 이 기능들을 완전히 제거하면 Agent Loop 자체가 사라집니다.
다음으로, 대체할 수 있는 구현이 많은지 확인합니다.
Persistence, Sandbox, Model Adapter, Telemetry는 모두 구현 선택지가 매우 많습니다. 특정 구현을 Core에 고정하면 교체 비용이 빠르게 증가합니다.
마지막으로, 독립적인 라이프사이클이 필요한지 확인합니다.
어떤 기능이 동적 로딩, 언로드, 격리, 재로딩을 요구하거나 다른 기능에 의존한다면 이미 컴포넌트 속성을 갖추었으므로 Harness에 맡기는 편이 적합합니다.
다음과 같이 실용적인 판단 기준을 세울 수 있습니다.
Agent Core
负责不可再分的执行语义
Harness
负责能力的组合、替换与生命周期
이 경계는 Core가 극도로 작아야 한다는 것을 요구하지 않습니다. 지나치게 나누는 것도 이해 비용을 높입니다. 핵심은 모든 제품 기능이 같은 Loop를 수정해야만 연결되게 하는 일을 피하는 것입니다.
Pi는 비교적 보수적이고 이해하기 쉬운 방식을 보여줍니다. 안정적인 Runtime을 유지하고 Extension을 통해 Contributions와 Hooks를 확장합니다.
DeepSeek Harness가 목표를 더 넓힌 이후에는 다른 종류의 문제를 해결해야 합니다. 구성요소가 런타임에 나타나 사라지고 서로 의존할 때, 조합 관계를 어떻게 올바르게 유지할 것인가입니다.
다음 글에서는 Cordis의 배경에 있는 논문 《A Programming Paradigm for Spatiotemporal Composability》를 다룹니다. 이 논문은 이 문제에 더 일반적인 모델을 구축하려 합니다.
참고자료
[1] Pi Extensions: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md [2] Pi Coding Agent: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/README.md [3] Pi Extension 유형: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts [4] Pi Extension 통합 설계 논의: https://github.com/earendil-works/pi/issues/454 [5] DeepSeek Harness 아키텍처: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md