From 65e05612a1b58c714a450878446f62f635c65281 Mon Sep 17 00:00:00 2001 From: subochev Date: Fri, 18 Sep 2026 00:29:48 +0300 Subject: [PATCH] =?UTF-8?q?refactor(agentik-cli):=20=D0=B2=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20subcommands=20(conv=20ls?= =?UTF-8?q?/new/...)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - conv-ls/new/show/delete/rename -> вложенные под agentik-cli conv - ConvCommand — Subcommand-родитель, регистрирует 5 дочерних команд в init { subcommands(...) } - ConvSubcommand(name, description) extends AgentikSubcommand — базовый класс для всех conv-подкоманд (наследует --server/--id) Два гоччаса kotlinx.cli 0.3.6 которые пришлось обойти: 1. parent.execute() вызывается ПОСЛЕ leaf.execute() всегда когда leaf достигнут через parent. Если parent делает что-то в execute() — вывод дублируется после каждой дочерней команды. Фикс: ConvCommand.execute() = Unit (no-op). Дочерние команды смотрятся через 'agentik-cli conv --help'. 2. По умолчанию 'conv new --server ...' парсится как conv[--server ...] + позиционный arg 'new' на уровне родителя, и дочерняя команда не запускается. Фикс: ArgParser(strictSubcommandOptionsOrder = true) — все аргументы после имени subcommand передаются в его парсер. Smoke (linuxX64 kexe + JVM fatjar): conv ls/new/rename/show/delete + msgs/send/interrupt/info работают. --- agentik-cli/README.md | 59 ++++++++++++------- .../kotlin/pw/binom/agentik/cli/AgentikCli.kt | 46 +++++++-------- .../binom/agentik/cli/commands/ConvCommand.kt | 32 ++++++++++ .../cli/commands/ConvDeleteSubcommand.kt | 2 +- .../agentik/cli/commands/ConvLsSubcommand.kt | 2 +- .../agentik/cli/commands/ConvNewSubcommand.kt | 2 +- .../cli/commands/ConvRenameSubcommand.kt | 2 +- .../cli/commands/ConvShowSubcommand.kt | 2 +- 8 files changed, 98 insertions(+), 49 deletions(-) create mode 100644 agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvCommand.kt diff --git a/agentik-cli/README.md b/agentik-cli/README.md index b2d4ca6..3bd0cc5 100644 --- a/agentik-cli/README.md +++ b/agentik-cli/README.md @@ -7,7 +7,7 @@ стрим ответа `send` идёт в stdout построчно, никакого embedded-REPL. Решает: быстрый способ дёрнуть агента из shell-скрипта или руками, -не поднимая отдельную TUI-сессию. +не поднимая отдельную TUI-сессии. ## Платформы @@ -24,28 +24,32 @@ ## Подкоманды ``` -agentik-cli [options] +agentik-cli [args...] -Subcommands: - conv-ls Список диалогов агента - conv-new Создать диалог; печатает id - conv-show Метаданные диалога - conv-delete Удалить диалог - conv-rename Переименовать диалог - msgs Показать сообщения диалога - send Отправить user-ход и стримить ответ - interrupt Прервать текущий ход диалога - info Показать server URL и agent id +Команды верхнего уровня: + conv операции над диалогами (см. ниже) + msgs [--limit N] показать сообщения + send отправить ход, стримит response-события в stdout + interrupt прервать текущий ход + info показать конфиг (server URL + agent id) + +Подкоманды `conv`: + conv ls список диалогов + conv new [--temp] создать диалог, печатает id + conv show метаданные диалога + conv delete удалить диалог + conv rename переименовать ``` `--server URL` и `--id ID` (env: `AGENTIK_SERVER`, `AGENTIK_AGENT_ID`) задаются **после** имени subcommand'а — kotlinx.cli не шарит опции -родителя в subcommand. Пример: +родителя в subcommand. Примеры: ```bash -agentik-cli conv-ls --server http://192.168.76.166:8080/agentik -agentik-cli conv-new --server http://localhost:8080/agentik +agentik-cli conv ls --server http://192.168.76.166:8080/agentik +agentik-cli conv new --server http://localhost:8080/agentik agentik-cli send --server http://localhost:8080/agentik conv-abc "привет" +agentik-cli info # через AGENTIK_SERVER env-переменную ``` ## Как запустить @@ -55,14 +59,14 @@ agentik-cli send --server http://localhost:8080/agentik conv-abc "привет" ```bash ./gradlew :agentik-cli:shadowJar java --enable-native-access=ALL-UNNAMED \ - -jar agentik-cli/build/libs/agentik-cli-0.1.0-SNAPSHOT-all.jar --help + -jar agentik-cli/build/libs/agentik-cli-0.1.0-SNAPSHOT-all.jar conv --help ``` ### Native linuxX64 ```bash ./gradlew :agentik-cli:linkReleaseExecutableLinuxX64 -./agentik-cli/build/bin/linuxX64/releaseExecutable/agentik-cli.kexe --help +./agentik-cli/build/bin/linuxX64/releaseExecutable/agentik-cli.kexe conv --help ``` ### Native macOS / Windows @@ -79,14 +83,14 @@ CI-ноут: запускать `./gradlew :agentik-cli:linkReleaseExecutableMac ```bash # Список диалогов (таблица) -agentik-cli conv-ls --server http://localhost:8080/agentik +agentik-cli conv ls --server http://localhost:8080/agentik # Создать диалог -ID=$(agentik-cli conv-new --server http://localhost:8080/agentik) +ID=$(agentik-cli conv new --server http://localhost:8080/agentik) echo "new conv: $ID" # Переименовать -agentik-cli conv-rename --server http://localhost:8080/agentik "$ID" "мой чат" +agentik-cli conv rename --server http://localhost:8080/agentik "$ID" "мой чат" # Отправить ход и стримить ответ agentik-cli send --server http://localhost:8080/agentik "$ID" "2+2" @@ -98,7 +102,7 @@ agentik-cli msgs --server http://localhost:8080/agentik "$ID" --limit 10 agentik-cli interrupt --server http://localhost:8080/agentik "$ID" # Удалить -agentik-cli conv-delete --server http://localhost:8080/agentik "$ID" +agentik-cli conv delete --server http://localhost:8080/agentik "$ID" # Через env-переменную AGENTIK_SERVER=http://localhost:8080/agentik agentik-cli info @@ -146,6 +150,19 @@ event End требует `@OptIn(ExperimentalForeignApi::class)`). - **Stdout / exit code** — работают на K/N через корутины. +## Готчасы kotlinx.cli + +- **Вложенные subcommands + parent.execute().** В kotlinx.cli 0.3.6 + `parent.execute()` вызывается ПОСЛЕ `leaf.execute()` всегда, + когда leaf был достигнут через parent. Поэтому `ConvCommand.execute()` + сделан no-op (`override fun execute() = Unit`), иначе вывод + дочерней команды дублируется выводом родителя. Дочерние команды + смотрятся через `agentik-cli conv --help`. +- **strictSubcommandOptionsOrder.** Без этого флага `conv new --server ...` + парсится как `conv [--server ...]` + позиционный аргумент `new` + на уровне родителя — и дочерняя команда не запускается. + В `ArgParser` сразу включается `strictSubcommandOptionsOrder = true`. + ## Тесты Тесты для подкоманд пока не написаны (TODO). Базовый smoke diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/AgentikCli.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/AgentikCli.kt index 956acee..27d62d9 100644 --- a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/AgentikCli.kt +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/AgentikCli.kt @@ -5,11 +5,7 @@ import kotlinx.cli.ArgType import kotlinx.cli.ExperimentalCli import kotlinx.cli.Subcommand import kotlinx.cli.default -import pw.binom.agentik.cli.commands.ConvDeleteSubcommand -import pw.binom.agentik.cli.commands.ConvLsSubcommand -import pw.binom.agentik.cli.commands.ConvNewSubcommand -import pw.binom.agentik.cli.commands.ConvRenameSubcommand -import pw.binom.agentik.cli.commands.ConvShowSubcommand +import pw.binom.agentik.cli.commands.ConvCommand import pw.binom.agentik.cli.commands.InfoSubcommand import pw.binom.agentik.cli.commands.InterruptSubcommand import pw.binom.agentik.cli.commands.MsgsSubcommand @@ -32,36 +28,40 @@ internal fun defaultAgentId(): String = platformEnv("AGENTIK_AGENT_ID") ?: "cli" * agentik-cli <command> [args...] * * Commands: - * conv-ls список диалогов - * conv-new [--temp] создать диалог, печатает id - * conv-show <id> метаданные диалога - * conv-delete <id> удалить диалог - * conv-rename <id> <title> переименовать - * msgs <id> [--limit N] показать сообщения - * send <id> <text...> отправить ход, стримит response-события в stdout - * interrupt <id> прервать текущий ход - * info показать конфиг + * conv ls|new|show|delete|rename операции над диалогами + * msgs <id> [--limit N] показать сообщения + * send <id> <text...> отправить ход, стримит response-события в stdout + * interrupt <id> прервать текущий ход + * info показать конфиг * * `--server` и `--id` задаются ПОСЛЕ имени subcommand'а (т.е. - * `agentik-cli conv-ls --server http://...`), не до — kotlinx.cli не + * `agentik-cli conv ls --server http://...`), не до — kotlinx.cli не * шарит опции родителя в subcommand. + * + * Вложенные subcommands (`conv ls`, `conv new`, ...) реализованы + * через [Subcommand.subcommands]: `conv` сам — subcommand, и его + * дочерние команды (`ls`, `new`, `show`, `delete`, `rename`) + * регистрируются у него. */ @OptIn(ExperimentalCli::class) fun runAgentikCli(args: Array<String>) { - val parser = ArgParser(programName = "agentik-cli") + val parser = ArgParser( + programName = "agentik-cli", + // Все аргументы после имени subcommand должны передаваться + // В subcommand-парсер, а не парситься на уровне родителя. + // Без этого `conv new --server ...` парсится как `conv [--server ...]` + // + аргумент "new" → execute родителя, без вложенной команды. + strictSubcommandOptionsOrder = true, + ) - val subcommands: List<Subcommand> = listOf( - ConvLsSubcommand(), - ConvNewSubcommand(), - ConvShowSubcommand(), - ConvDeleteSubcommand(), - ConvRenameSubcommand(), + val conv = ConvCommand() + parser.subcommands( + conv, MsgsSubcommand(), SendSubcommand(), InterruptSubcommand(), InfoSubcommand(), ) - subcommands.forEach { parser.subcommands(it) } parser.parse(args) } diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvCommand.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvCommand.kt new file mode 100644 index 0000000..792a36f --- /dev/null +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvCommand.kt @@ -0,0 +1,32 @@ +package pw.binom.agentik.cli.commands + +import kotlinx.cli.ExperimentalCli +import kotlinx.cli.Subcommand +import pw.binom.agentik.cli.AgentikSubcommand + +/** + * Родительская группа `conv`: операции над диалогами. + * + * Сама команда `agentik-cli conv` (без подкоманды) — no-op: + * в kotlinx.cli parent.execute() вызывается ПОСЛЕ leaf.execute(), + * поэтому любая работа в execute() дублирует вывод дочерней команды. + * Для просмотра дочерних команд есть `agentik-cli conv --help`. + * + * Дочерние команды регистрируются через [subcommands] в конструкторе. + */ +@OptIn(ExperimentalCli::class) +class ConvCommand : Subcommand("conv", "Операции над диалогами") { + init { + subcommands( + ConvLsSubcommand(), + ConvNewSubcommand(), + ConvShowSubcommand(), + ConvDeleteSubcommand(), + ConvRenameSubcommand(), + ) + } + + override fun execute() = Unit +} + +abstract class ConvSubcommand(name: String, description: String) : AgentikSubcommand(name, description) diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvDeleteSubcommand.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvDeleteSubcommand.kt index 07d8617..3842210 100644 --- a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvDeleteSubcommand.kt +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvDeleteSubcommand.kt @@ -4,7 +4,7 @@ import kotlinx.cli.ArgType import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.client.AgentikAgent -class ConvDeleteSubcommand : AgentikSubcommand("conv-delete", "Удалить диалог") { +class ConvDeleteSubcommand : ConvSubcommand("delete", "Удалить диалог") { val id by argument(ArgType.String, description = "ID диалога") override fun execute() = kotlinx.coroutines.runBlocking { diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvLsSubcommand.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvLsSubcommand.kt index f4cd7a9..8da347d 100644 --- a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvLsSubcommand.kt +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvLsSubcommand.kt @@ -6,7 +6,7 @@ import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.client.AgentikAgent import pw.binom.agentik.proto.Agent -class ConvLsSubcommand : AgentikSubcommand("conv-ls", "Список диалогов агента") { +class ConvLsSubcommand : ConvSubcommand("ls", "Список диалогов агента") { val limit by option(ArgType.Int, fullName = "limit", description = "Максимум диалогов").default(Agent.PAGE_SIZE) override fun execute() = kotlinx.coroutines.runBlocking { diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvNewSubcommand.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvNewSubcommand.kt index ab028a1..979237c 100644 --- a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvNewSubcommand.kt +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvNewSubcommand.kt @@ -5,7 +5,7 @@ import kotlinx.cli.default import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.client.AgentikAgent -class ConvNewSubcommand : AgentikSubcommand("conv-new", "Создать диалог; печатает id") { +class ConvNewSubcommand : ConvSubcommand("new", "Создать диалог; печатает id") { val temp by option(ArgType.Boolean, fullName = "temp", description = "Временный диалог").default(false) override fun execute() = kotlinx.coroutines.runBlocking { diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvRenameSubcommand.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvRenameSubcommand.kt index ada3c38..537bb27 100644 --- a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvRenameSubcommand.kt +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvRenameSubcommand.kt @@ -4,7 +4,7 @@ import kotlinx.cli.ArgType import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.client.AgentikAgent -class ConvRenameSubcommand : AgentikSubcommand("conv-rename", "Переименовать диалог") { +class ConvRenameSubcommand : ConvSubcommand("rename", "Переименовать диалог") { val id by argument(ArgType.String, description = "ID диалога") val title by argument(ArgType.String, description = "Новое название") diff --git a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvShowSubcommand.kt b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvShowSubcommand.kt index 820b3a2..83830f2 100644 --- a/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvShowSubcommand.kt +++ b/agentik-cli/src/commonMain/kotlin/pw/binom/agentik/cli/commands/ConvShowSubcommand.kt @@ -4,7 +4,7 @@ import kotlinx.cli.ArgType import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.client.AgentikAgent -class ConvShowSubcommand : AgentikSubcommand("conv-show", "Метаданные диалога") { +class ConvShowSubcommand : ConvSubcommand("show", "Метаданные диалога") { val id by argument(ArgType.String, description = "ID диалога") override fun execute() = kotlinx.coroutines.runBlocking {