refactor(agentik-cli): вложенные subcommands (conv ls/new/...)
ci / JVM build + tests (push) Failing after 2m0s
release / Publish KMP libraries → caffeine Nexus (release) Successful in 5m35s

- 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 работают.
This commit is contained in:
2026-09-18 00:29:48 +03:00
parent 850ee99cb6
commit 65e05612a1
8 changed files with 98 additions and 49 deletions
+38 -21
View File
@@ -7,7 +7,7 @@
стрим ответа `send` идёт в stdout построчно, никакого embedded-REPL.
Решает: быстрый способ дёрнуть агента из shell-скрипта или руками,
не поднимая отдельную TUI-сессию.
не поднимая отдельную TUI-сессии.
## Платформы
@@ -24,28 +24,32 @@
## Подкоманды
```
agentik-cli <subcommand> [options]
agentik-cli <command> [args...]
Subcommands:
conv-ls Список диалогов агента
conv-new Создать диалог; печатает id
conv-show Метаданные диалога
conv-delete Удалить диалог
conv-rename Переименовать диалог
msgs Показать сообщения диалога
send Отправить user-ход и стримить ответ
interrupt Прервать текущий ход диалога
info Показать server URL и agent id
Команды верхнего уровня:
conv <subcommand> операции над диалогами (см. ниже)
msgs <id> [--limit N] показать сообщения
send <id> <text...> отправить ход, стримит response-события в stdout
interrupt <id> прервать текущий ход
info показать конфиг (server URL + agent id)
Подкоманды `conv`:
conv ls список диалогов
conv new [--temp] создать диалог, печатает id
conv show <id> метаданные диалога
conv delete <id> удалить диалог
conv rename <id> <title> переименовать
```
`--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
@@ -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)
}
@@ -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)
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 = "Новое название")
@@ -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 {