지난 두 편을 통해 비교적 명확한 런타임 의미를 도출했다:
이번 글에서는 완전한 Cordis나 DeepSeek Harness를 구현하지 않는다. 목표는 단 하나다. 동적 의존성 시스템에서 가장 핵심적인 라이프사이클 동작을 검증하는 것이다.
다음 결과를 얻어야 한다:
A provides db
B requires db
B provides repo
C requires repo
모두 로드한 뒤에는:
A ACTIVE
B ACTIVE
C ACTIVE
A를 언로드하면:
db disappears
↓
B unloads
↓
repo disappears
↓
C unloads
A를 다시 로드하면:
db appears
↓
B reloads
↓
repo appears
↓
C reloads
구현은 이번 모듈의 demo/ 디렉터리에 들어가 있으며, Node.js로 바로 실행할 수 있다.
Runtime은 네 가지 핵심 객체만 유지한다
Demo의 PluginRuntime이 유지한다:
services
service name → provider
fibers
plugin id → Fiber
consumers
service name → Set<Fiber>
providerVersion
monotonic provider identity
각 항목은 다음을 뜻한다:
services
service name → provider
fibers
plugin id → Fiber
consumers
service name → Set<Fiber>
providerVersion
monotonic provider identity
구현을 더 쉽게 관찰하기 위해 Demo는 역방향 의존성 인덱스를 명시적으로 유지한다. Cordis의 현재 vendored 구현에서 reflect.notify()는 Registry의 Fiber를 순회한 뒤 inject와 isolation scope를 기준으로 필터링한다. 의미는 같지만 데이터 구조 선택이 다르다. [1]
Fiber 상태는 다음과 같이 단순화했다:
PENDING
LOADING
ACTIVE
UNLOADING
DISPOSED
FAILED, Config, Scope, Isolation, HMR 등의 기능은 구현하지 않았다.
Plugin 정의
각 Plugin은 두 가지만 선언한다:
requires는 Coeffect로, 플러그인이 실행 환경에 내리는 요구 사항이다.
setup()에서 다음과 같이 호출한다:
이 호출로 Effect가 생성된다.
이 두 부분이 함께 Plugin의 동적 라이프사이클을 결정한다.
Effect 소유권
Context의 구현은 매우 작다:
핵심은 provide() 자체도 effect()를 통해 설치된다는 점이다.
따라서:
Fiber unload
↓
dispose effects
↓
remove provided service
Service 라이프사이클이 Fiber에 자연스럽게 종속된다.
reconcile에는 폴링이 필요 없다
Runtime은 Service가 변경되면 다음을 호출한다:
해당 Service에 의존하는 Fiber만 가져온다:
그다음 다음을 호출한다:
Fiber는 의존성을 바탕으로 새로운 epoch를 계산한다:
의존성이 하나라도 없으면:
epoch = null
Fiber는 PENDING 상태여야 한다.
모든 의존성이 존재하면:
db@1|cache@2
Fiber가 ACTIVE 상태가 될 수 있다.
Provider가 교체된 후에는 Service name이 변하지 않아도:
db@1
↓
db@7
epoch도 달라져 reload가 트리거된다.
라이프사이클 도중 발생하는 추가 변화 처리
놓치기 쉬운 문제가 하나 있다:
B 正在 UNLOADING
↓
依赖重新出现
이때 Runtime이 바로 load()를 호출하면, 같은 Plugin이 setup과 cleanup을 동시에 실행하게 된다.
Demo는 직렬 reconcile loop를 사용한다:
새로운 환경 변화는 다음 값만 설정한다:
needsReconcile = true
현재 라이프사이클 동작이 끝난 다음, 최신 환경 상태를 기준으로 다시 한 번 계산한다.
이것은 Cordis가 epoch + inertia로 해결하는 문제와 같다. Fiber의 라이프사이클 전환은 직렬화해야 하지만, 목표 의존 상태는 전환이 진행되는 동안 계속 변할 수 있다. [2]
의존 체인이 전파되는 방식
Demo에는 세 개의 Plugin이 있다:
초기 설치 시 일부러 잘못된 순서를 사용할 수도 있다:
앞선 두 Fiber는 의존 조건이 충족되지 않아 PENDING 상태에 머문다:
feature PENDING
repository PENDING
database ACTIVE
database가 db를 제공한 뒤에는:
db changed
↓
repository reconcile
↓
repository ACTIVE
↓
repo changed
↓
feature reconcile
↓
feature ACTIVE
로드 순서는 이제 의존 관계에서 도출되며, 호출자가 직접 보장할 필요가 없다.
Provider 언로드
다음과 같이 실행한다:
database Fiber가 db를 정리한다:
remove db
↓
notify repository
repository는 UNLOADING 상태로 들어가며, 자신이 제공하는 repo도 정리한다:
remove repo
↓
notify feature
그다음 feature가 언로드된다.
최종 상태는 다음과 같다:
database DISPOSED
repository PENDING
feature PENDING
여기에는 주목할 만한 경계가 있다.
repository와 feature는 삭제되지 않는다. 이미 설치된 Plugin으로 남아 있을 뿐이며, 현재 의존 환경이 실행을 허용하지 않는 상태다.
따라서 다음 두 상태를 구분해야 한다:
installed
그리고:
active
동적 플러그인 시스템에서는 일반적으로 이 두 상태를 하나로 합칠 수 없다.
Provider 복구
database를 다시 설치한다:
새 Provider는 새로운 version을 가진다.
db@2 appears
↓
repository reload
↓
repo appears
↓
feature reload
하위 Plugin은 다시 등록할 필요도, Application이 수동으로 시작할 필요도 없다.
Runtime이 의존 선언을 바탕으로 활성 체인 전체를 복원한다.
Demo에서 의도적으로 생략한 것
실제 Cordis는 다음 기능도 처리해야 한다:
하지만 이 기능들은 Demo가 검증하려는 핵심 모델을 바꾸지 않는다:
Service change
↓
dependent Fiber reconcile
↓
load / unload
↓
Effect apply / revert
↓
more Service changes
의존 그래프 전체는 이 국소 규칙을 통해 새로운 안정 상태에 점진적으로 도달한다.
Demo에서 Agent Harness로 돌아가기
Demo의 Service 이름을 Agent 기능으로 바꾸면:
llm
tools
sessions
sandbox
agentLoop
간소화된 Harness를 얻을 수 있다:
LLM Provider를 교체하면:
old llm provider disappears
↓
agentLoop temporarily unloads
↓
new llm provider appears
↓
agentLoop reloads against new dependency epoch
이것이 바로 Everything is a Plugin가 성립하는 데 필요한 기반 라이프사이클 기능이다.
세 번째 모듈은 여기까지 다음과 같은 완전한 진화 경로를 형성했다:
다음 모듈에서는 시선을 Web으로 옮긴다. Agent 실행 라이프사이클과 브라우저 연결을 분리한 이후 Server Runtime, SSE, Replay, Client Runtime, UI Projection을 어떻게 구성해야 할지 다룬다.
Demo
실행:
cd demo
node src/demo.mjs
파일:
demo/
├── src/
│ ├── runtime.mjs
│ └── demo.mjs
└── README.md
참고 자료
[1] Cordis 소스: https://github.com/deepseek-ai/deepseek-harness/tree/master/vendor/cordis/src
[2] Cordis 논문: https://github.com/cordiverse/paper
[3] DeepSeek Harness 아키텍처: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md