refactor(agentik-cli): вложенные subcommands (conv ls/new/...)
- 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:
+38
-21
@@ -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)
|
||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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 = "Новое название")
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user