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. стрим ответа `send` идёт в stdout построчно, никакого embedded-REPL.
Решает: быстрый способ дёрнуть агента из shell-скрипта или руками, Решает: быстрый способ дёрнуть агента из shell-скрипта или руками,
не поднимая отдельную TUI-сессию. не поднимая отдельную TUI-сессии.
## Платформы ## Платформы
@@ -24,28 +24,32 @@
## Подкоманды ## Подкоманды
``` ```
agentik-cli <subcommand> [options] agentik-cli <command> [args...]
Subcommands: Команды верхнего уровня:
conv-ls Список диалогов агента conv <subcommand> операции над диалогами (см. ниже)
conv-new Создать диалог; печатает id msgs <id> [--limit N] показать сообщения
conv-show Метаданные диалога send <id> <text...> отправить ход, стримит response-события в stdout
conv-delete Удалить диалог interrupt <id> прервать текущий ход
conv-rename Переименовать диалог info показать конфиг (server URL + agent id)
msgs Показать сообщения диалога
send Отправить user-ход и стримить ответ Подкоманды `conv`:
interrupt Прервать текущий ход диалога conv ls список диалогов
info Показать server URL и agent id conv new [--temp] создать диалог, печатает id
conv show <id> метаданные диалога
conv delete <id> удалить диалог
conv rename <id> <title> переименовать
``` ```
`--server URL` и `--id ID` (env: `AGENTIK_SERVER`, `AGENTIK_AGENT_ID`) `--server URL` и `--id ID` (env: `AGENTIK_SERVER`, `AGENTIK_AGENT_ID`)
задаются **после** имени subcommand'а — kotlinx.cli не шарит опции задаются **после** имени subcommand'а — kotlinx.cli не шарит опции
родителя в subcommand. Пример: родителя в subcommand. Примеры:
```bash ```bash
agentik-cli conv-ls --server http://192.168.76.166: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 conv new --server http://localhost:8080/agentik
agentik-cli send --server http://localhost:8080/agentik conv-abc "привет" 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 ```bash
./gradlew :agentik-cli:shadowJar ./gradlew :agentik-cli:shadowJar
java --enable-native-access=ALL-UNNAMED \ 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 ### Native linuxX64
```bash ```bash
./gradlew :agentik-cli:linkReleaseExecutableLinuxX64 ./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 ### Native macOS / Windows
@@ -79,14 +83,14 @@ CI-ноут: запускать `./gradlew :agentik-cli:linkReleaseExecutableMac
```bash ```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" 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" 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 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-переменную # Через env-переменную
AGENTIK_SERVER=http://localhost:8080/agentik agentik-cli info AGENTIK_SERVER=http://localhost:8080/agentik agentik-cli info
@@ -146,6 +150,19 @@ event End
требует `@OptIn(ExperimentalForeignApi::class)`). требует `@OptIn(ExperimentalForeignApi::class)`).
- **Stdout / exit code** — работают на K/N через корутины. - **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 Тесты для подкоманд пока не написаны (TODO). Базовый smoke
@@ -5,11 +5,7 @@ import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand import kotlinx.cli.Subcommand
import kotlinx.cli.default import kotlinx.cli.default
import pw.binom.agentik.cli.commands.ConvDeleteSubcommand import pw.binom.agentik.cli.commands.ConvCommand
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.InfoSubcommand import pw.binom.agentik.cli.commands.InfoSubcommand
import pw.binom.agentik.cli.commands.InterruptSubcommand import pw.binom.agentik.cli.commands.InterruptSubcommand
import pw.binom.agentik.cli.commands.MsgsSubcommand import pw.binom.agentik.cli.commands.MsgsSubcommand
@@ -32,36 +28,40 @@ internal fun defaultAgentId(): String = platformEnv("AGENTIK_AGENT_ID") ?: "cli"
* agentik-cli <command> [args...] * agentik-cli <command> [args...]
* *
* Commands: * Commands:
* conv-ls список диалогов * conv ls|new|show|delete|rename операции над диалогами
* conv-new [--temp] создать диалог, печатает id
* conv-show <id> метаданные диалога
* conv-delete <id> удалить диалог
* conv-rename <id> <title> переименовать
* msgs <id> [--limit N] показать сообщения * msgs <id> [--limit N] показать сообщения
* send <id> <text...> отправить ход, стримит response-события в stdout * send <id> <text...> отправить ход, стримит response-события в stdout
* interrupt <id> прервать текущий ход * interrupt <id> прервать текущий ход
* info показать конфиг * info показать конфиг
* *
* `--server` и `--id` задаются ПОСЛЕ имени subcommand'а (т.е. * `--server` и `--id` задаются ПОСЛЕ имени subcommand'а (т.е.
* `agentik-cli conv-ls --server http://...`), не до — kotlinx.cli не * `agentik-cli conv ls --server http://...`), не до — kotlinx.cli не
* шарит опции родителя в subcommand. * шарит опции родителя в subcommand.
*
* Вложенные subcommands (`conv ls`, `conv new`, ...) реализованы
* через [Subcommand.subcommands]: `conv` сам — subcommand, и его
* дочерние команды (`ls`, `new`, `show`, `delete`, `rename`)
* регистрируются у него.
*/ */
@OptIn(ExperimentalCli::class) @OptIn(ExperimentalCli::class)
fun runAgentikCli(args: Array<String>) { 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( val conv = ConvCommand()
ConvLsSubcommand(), parser.subcommands(
ConvNewSubcommand(), conv,
ConvShowSubcommand(),
ConvDeleteSubcommand(),
ConvRenameSubcommand(),
MsgsSubcommand(), MsgsSubcommand(),
SendSubcommand(), SendSubcommand(),
InterruptSubcommand(), InterruptSubcommand(),
InfoSubcommand(), InfoSubcommand(),
) )
subcommands.forEach { parser.subcommands(it) }
parser.parse(args) 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.cli.AgentikSubcommand
import pw.binom.agentik.client.AgentikAgent import pw.binom.agentik.client.AgentikAgent
class ConvDeleteSubcommand : AgentikSubcommand("conv-delete", "Удалить диалог") { class ConvDeleteSubcommand : ConvSubcommand("delete", "Удалить диалог") {
val id by argument(ArgType.String, description = "ID диалога") val id by argument(ArgType.String, description = "ID диалога")
override fun execute() = kotlinx.coroutines.runBlocking { 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.client.AgentikAgent
import pw.binom.agentik.proto.Agent 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) val limit by option(ArgType.Int, fullName = "limit", description = "Максимум диалогов").default(Agent.PAGE_SIZE)
override fun execute() = kotlinx.coroutines.runBlocking { override fun execute() = kotlinx.coroutines.runBlocking {
@@ -5,7 +5,7 @@ import kotlinx.cli.default
import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.cli.AgentikSubcommand
import pw.binom.agentik.client.AgentikAgent 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) val temp by option(ArgType.Boolean, fullName = "temp", description = "Временный диалог").default(false)
override fun execute() = kotlinx.coroutines.runBlocking { override fun execute() = kotlinx.coroutines.runBlocking {
@@ -4,7 +4,7 @@ import kotlinx.cli.ArgType
import pw.binom.agentik.cli.AgentikSubcommand import pw.binom.agentik.cli.AgentikSubcommand
import pw.binom.agentik.client.AgentikAgent import pw.binom.agentik.client.AgentikAgent
class ConvRenameSubcommand : AgentikSubcommand("conv-rename", "Переименовать диалог") { class ConvRenameSubcommand : ConvSubcommand("rename", "Переименовать диалог") {
val id by argument(ArgType.String, description = "ID диалога") val id by argument(ArgType.String, description = "ID диалога")
val title by argument(ArgType.String, description = "Новое название") 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.cli.AgentikSubcommand
import pw.binom.agentik.client.AgentikAgent import pw.binom.agentik.client.AgentikAgent
class ConvShowSubcommand : AgentikSubcommand("conv-show", "Метаданные диалога") { class ConvShowSubcommand : ConvSubcommand("show", "Метаданные диалога") {
val id by argument(ArgType.String, description = "ID диалога") val id by argument(ArgType.String, description = "ID диалога")
override fun execute() = kotlinx.coroutines.runBlocking { override fun execute() = kotlinx.coroutines.runBlocking {