2e1387273a
Новый KMP-модуль :agent-toolsets с основными абстракциями для тулсетов: - ToolsetContribution(name, description, tools: List<ToolEntry>) — декларация тулсета: имя + описание + список входящих LiteTool'ов с именами. - ToolsetContext + Logger + NoOpLogger — что тулсеты получают при активации. - ToolsetRegistry — реестр тулсетов с Mutex-защитой; методы activate/deactivate/isActive/activeNames/inactiveNames/activeTools/ findByName/findOwnerByToolName. - ToolsetDispatchPolicy — диспетчер с прощающей auto-activation: если тул из неактивного тулсета вызван — молча активирует тулсет и выполняет. Если тул вообще неизвестен — fallback в BaseToolDispatcher (плоские тулы вне toolsets). - EnableToolsetTool / DisableToolsetTool — встроенные LiteTool'ы (4-case контракт зафиксирован в docs/TOOLSETS-PLAN.md): activate/deactivate с равномерным сообщением 'X deactivated' независимо от того, был ли он активен. - SyncLiteTool — обёртка suspend-handler'а в синхронный LiteTool (через runBlocking). LiteTool.invoke синхронен по контракту litert-kmp. Дизайн: - :agent-toolsets НЕ зависит от :standalone — может быть переиспользован в Android-сборке и любом LiteTool-агенте. - Модуль KMP (jvm + native), общие интерфейсы в commonMain, JVM-специфика только в SyncLiteTool (runBlocking). - ToolsetContext минимален (logger); storage/skill добавятся в commit 5+. Тесты: 29 новых покрывают activate/deactivate/idempotency, activeTools, findOwnerByToolName, auto-activation в диспетчере, fallback в base, оба контракта enable/disable со всеми 4 кейсами. Tests: 328/328 green (299 ранее + 29 в :agent-toolsets)
116 lines
5.3 KiB
Kotlin
116 lines
5.3 KiB
Kotlin
package pw.binom.agentik.toolsets
|
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
import kotlinx.coroutines.sync.withLock
|
|
import pw.binom.litert.LiteTool
|
|
|
|
/**
|
|
* Реестр тулсетов: хранит список доступных [ToolsetContribution]-ов и
|
|
* отслеживает, какие из них сейчас активны.
|
|
*
|
|
* Потокобезопасен (`Mutex` вокруг всех мутаций). Один экземпляр на агента —
|
|
* разделяется между ChatAgent и диспетчером.
|
|
*
|
|
* Диспетчер тулов (см. [ToolsetDispatchPolicy]) использует [findOwnerByToolName]
|
|
* чтобы:
|
|
* 1. Найти активный тул по имени — диспетчировать напрямую.
|
|
* 2. Если тул принадлежит неактивному тулсету — молча его активировать.
|
|
* 3. Если тул вообще не найден — передать в fallback-диспетчер
|
|
* (для «плоских» тулов вне toolsets).
|
|
*
|
|
* Модель может явно управлять состоянием через тулы `enable_toolset` /
|
|
* `disable_toolset` (см. [EnableToolsetTool], [DisableToolsetTool]).
|
|
*/
|
|
class ToolsetRegistry(
|
|
private val contributions: List<ToolsetContribution>,
|
|
private val context: ToolsetContext = NoOpToolsetContext,
|
|
) : AutoCloseable {
|
|
|
|
private val active: MutableSet<String> = mutableSetOf()
|
|
private val lock = Mutex()
|
|
|
|
/** Все зарегистрированные тулсеты (read-only). */
|
|
fun all(): List<ToolsetContribution> = contributions
|
|
|
|
/** Имена всех зарегистрированных тулсетов (для prompt section и диагностики). */
|
|
fun names(): List<String> = contributions.map { it.name }
|
|
|
|
/** Найти тулсет по имени (или null). */
|
|
fun findByName(name: String): ToolsetContribution? =
|
|
contributions.firstOrNull { it.name == name }
|
|
|
|
/**
|
|
* Найти тулсет, владеющий тулом с данным именем. Перебирает все
|
|
* зарегистрированные тулсеты, у каждого смотрит [ToolsetContribution.tools].
|
|
*
|
|
* Используется диспетчером для auto-activation: если модель вызвала тул из
|
|
* неактивного тулсета — мы молча его активируем и выполняем.
|
|
*/
|
|
fun findOwnerByToolName(toolName: String): Pair<ToolsetContribution, ToolsetContribution.ToolEntry>? {
|
|
for (c in contributions) {
|
|
val entry = c.tools.firstOrNull { it.toolName == toolName }
|
|
if (entry != null) return c to entry
|
|
}
|
|
return null
|
|
}
|
|
|
|
suspend fun isActive(name: String): Boolean = lock.withLock { active.contains(name) }
|
|
|
|
/**
|
|
* Активировать тулсет. Если уже активен — no-op. Возвращает `true`, если
|
|
* состояние изменилось (т.е. тулсет был неактивен и теперь активен).
|
|
*/
|
|
suspend fun activate(name: String): Boolean = lock.withLock {
|
|
active.add(name)
|
|
}
|
|
|
|
/**
|
|
* Деактивировать тулсет. Если и так неактивен — no-op. Возвращает `true`,
|
|
* если состояние изменилось.
|
|
*/
|
|
suspend fun deactivate(name: String): Boolean = lock.withLock {
|
|
active.remove(name)
|
|
}
|
|
|
|
suspend fun activeNames(): List<String> = lock.withLock { active.toList() }
|
|
|
|
suspend fun inactiveNames(): List<String> = lock.withLock {
|
|
contributions.map { it.name }.filter { it !in active }
|
|
}
|
|
|
|
/**
|
|
* Список всех активных тулов (для передачи в LiteConversationConfig.tools).
|
|
* Вызывает `LiteTool.describe()` каждого тула — безопасно для типичных
|
|
* stateless тулов.
|
|
*/
|
|
suspend fun activeTools(): List<LiteTool> {
|
|
val activeNames = activeNames()
|
|
return activeNames.mapNotNull { name ->
|
|
val contribution = findByName(name)
|
|
contribution?.tools?.map { it.tool }
|
|
}.flatten()
|
|
}
|
|
|
|
/** Тулсет-контекст, который передан конструктору. */
|
|
fun context(): ToolsetContext = context
|
|
|
|
override fun close() {
|
|
// no-op: нет внешних ресурсов. Сделано для удобства AutoCloseable-конвенции.
|
|
}
|
|
|
|
companion object {
|
|
/** Пустой реестр без единого тулсета. */
|
|
fun empty(context: ToolsetContext = NoOpToolsetContext): ToolsetRegistry =
|
|
ToolsetRegistry(emptyList(), context)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Дефолтный контекст для случая, когда внешний код не передал свой. Использует
|
|
* no-op логгер — события тулсетов (activate/deactivate/auto-activate) не
|
|
* пишутся никуда. Для prod-запуска передайте контекст с настоящим логгером.
|
|
*/
|
|
private val NoOpToolsetContext = object : ToolsetContext {
|
|
override val logger: Logger = NoOpLogger
|
|
}
|