기반 커널, Cordis

Cordis는 Koishi 커뮤니티에서 만든 TypeScript plugin 프레임워크입니다. DSH는 자체적인 plugin 시스템을 구현하지 않고 이를 사용하며, 이것이 바로 DSH plugin이 곧 Cordis plugin인 이유입니다.

Context는 서비스의 저장소입니다

모든 plugin은 관례적으로 ctx라고 명명된 context를 받습니다. Service는 ctx.tools, ctx.llm 또는 ctx.sessions와 같은 고정된 키 아래에 존재합니다. 툴 테이블이 필요한 plugin은 구체적인 구현체가 아닌 해당 키를 참조하므로, plugin이 인지하지 못한 상태에서 키 뒤의 구현체가 변경될 수 있습니다.

context
import type { Context } from 'cordis'

// Services live on the context under stable keys.
// A plugin reaches for the key, not for a concrete class.
export function apply(ctx: Context) {
  ctx.tools     // the tool table
  ctx.sessions  // the append-only session log
  ctx.llm       // whichever model adapter is loaded
}

플러그인은 Service를 구현하는 모든 것입니다

실제로는 apply(ctx) 본문을 가진 함수이거나, Cordis가 현재 컨텍스트에 수명 주기를 탑재하는 Service를 상속받은 클래스를 의미합니다. 등록 매니페스트는 없으며 그 외에 상속받아야 할 플러그인 기본 클래스도 없습니다.

로드 순서는 선언형이며, 순차적이지 않습니다

플러그인은 inject에 필요한 항목을 나열합니다. Cordis는 나열된 모든 서비스가 존재할 때까지 플러그인을 대기 상태로 유지한 다음 apply를 호출합니다. 부팅 순서를 수동으로 관리할 필요가 없으며, 의존성보다 먼저 도착한 플러그인은 충돌하는 대신 단순히 대기합니다.

inject
export const inject = ['tools', 'sessions']

export function apply(ctx: Context) {
  // Cordis holds this plugin in a pending state until both
  // services exist, so neither lookup below can be undefined.
  const session = ctx.sessions.current()
  ctx.tools.list().forEach((tool) => session.note(tool.name))
}

apply 내부에서는 inject에 명명된 모든 것이 존재함이 보장됩니다.

부작용은 되돌릴 수 있습니다

ctx.on과 ctx.effect를 통해 이루어진 등록은 추적됩니다. 플러그인이 언로드되거나 리로드될 때 Cordis는 해당 기록을 순회하며 각각을 되돌립니다. 리스너는 제거되고, 서비스는 해제되며, 타이머는 지워집니다. 이것이 플러그인의 핫 스와핑을 느린 메모리 누수가 아닌 안전한 작업으로 만드는 이유입니다.

reversible effect
export function apply(ctx: Context) {
  const dispose = ctx.tools.register('read_file', async ({ path }) => {
    return readFile(path, 'utf8')
  })

  // Returning the disposer is what makes the plugin removable:
  // unloading it takes the tool back out of the table.
  return dispose
}

이벤트를 전달하는 네 가지 방법

플러그인은 서비스뿐만 아니라 이벤트를 통해서도 서로 통신하며, 전달 모드에 따라 리스너가 수행할 수 있는 작업이 결정됩니다.

emit

발생 후 망각(Fire and forget)합니다. 모든 리스너가 실행되며 반환되는 값은 없습니다.

parallel

모든 리스너가 동시에 실행되며, 호출자는 모든 리스너가 완료될 때까지 대기합니다.

serial

리스너 중 하나가 값을 반환할 때까지 순차적으로 실행되며, 해당 값이 결과가 됩니다.

waterfall

각 리스너는 next를 수신하여 인수를 변환하거나, 다음으로 위임하거나, 단락(short-circuit)시킬 수 있습니다.

DSH에서 나타나는 방식

모든 DSH 기능은 컨텍스트상의 서비스입니다. 목록을 읽는 것이 하네스가 실제로 무엇으로 구성되어 있는지 이해하는 가장 빠른 방법입니다.

  • 모델 어댑터는 서비스 키를 점유합니다. 제공자를 교체한다는 것은 동일한 키에 대해 다른 플러그인을 로드하는 것을 의미합니다.
  • 각 도구는 자신을 도구 테이블에 되돌릴 수 있는 효과로 등록하므로, 도구를 제거하는 것은 해당 플러그인을 언로드하는 것입니다.
  • 세션 서비스는 추가 전용 로그를 소유합니다. 스토리지 백엔드는 그 뒤에 있는 별도의 플러그인입니다.
  • 에이전트 루프 자체가 하나의 플러그인이며, 이것이 Minimal이나 PTC와 같은 프리셋이 커널을 공유하면서도 크게 다를 수 있는 이유입니다.

Cordis 뒤에 있는 구성 가능성 모델은 DSH 저장소에서 링크된 시공간적 구성 가능성(spatiotemporal composability) 논문에 기술되어 있습니다.