Cordis, ядро в основе системы
Cordis - это фреймворк плагинов на TypeScript от сообщества Koishi. DSH не реализует собственную систему плагинов. Он использует эту систему, поэтому плагин DSH является плагином Cordis.
Контекст - это репозиторий служб
Каждый плагин получает контекст, который по соглашению называется ctx. Службы находятся в нем под постоянными ключами, такими как ctx.tools, ctx.llm или ctx.sessions. Плагин, которому нужна таблица инструментов, обращается к ключу, а не к конкретной реализации, поэтому реализация, скрытая за этим ключом, может меняться незаметно для плагина.
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) или класс, расширяющий Service, жизненный цикл которого Cordis встраивает в текущий контекст. Здесь нет манифеста регистрации или базового класса плагина, от которого нужно наследоваться.
Порядок загрузки объявляется, а не задается последовательно
Плагин перечисляет необходимые ему компоненты в inject. Cordis удерживает его в состоянии ожидания, пока не появится каждый из перечисленных сервисов, после чего вызывает apply. Никто не управляет порядком загрузки вручную, и плагин, который загружается раньше своей зависимости, просто ожидает вместо аварийного завершения.
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 проходит по этим записям и отменяет каждую из них. Слушатели удаляются, сервисы освобождаются, таймеры очищаются. Именно это делает горячую замену плагина безопасной, предотвращая постепенную утечку памяти.
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 Запустить и забыть. Выполняются все слушатели, результат не возвращается.
parallel Все слушатели выполняются одновременно, вызывающий объект ожидает завершения всех из них.
serial Слушатели выполняются по порядку до тех пор, пока один из них не вернет значение, которое станет итоговым результатом.
waterfall Каждый слушатель получает next и может преобразовывать аргументы, делегировать выполнение дальше или прервать цепочку.
Как это проявляется в DSH
Каждая возможность DSH является сервисом в контексте. Изучение этого списка - самый быстрый способ понять, из чего на самом деле состоит harness.
- Адаптер модели занимает ключ сервиса. Замена провайдеров означает загрузку другого плагина для того же самого ключа.
- Каждый инструмент регистрирует себя в таблице инструментов как обратимый эффект, поэтому удаление инструмента эквивалентно выгрузке его плагина.
- Сервис сессий владеет журналом, доступным только для добавления. Бэкенды хранилища являются отдельными плагинами, работающими за ним.
- Цикл агента сам по себе является плагином, поэтому пресеты вроде Minimal и PTC могут сильно различаться, используя при этом одно и то же ядро.
Модель компонуемости, лежащая в основе Cordis, описана в статье о пространственно-временной компонуемости, ссылка на которую есть в репозитории DSH.