proto/server/client: Agent.info, Working-маркер хода, /record, таймауты запросов
- proto: Agent.info (AgentInfo: name/description/usefulness) — человекочитаемое
имя отдельно от opaque id; сериализация и тесты
- server: GET {path} отдаёт Agent.info; GET /conversations/{id}/record —
ConversationRecord без handle'а (для клиентского кэша после AgentEvent.Created)
- outbox: Event.Working — первый event хода, эмитится из Conversation.send()
до LLM-цикла и turnLock, чтобы UI показал спиннер сразу
- standalone: AGENTIK_NAME/AGENTIK_DESCRIPTION/AGENTIK_USEFULNESS → AgentInfo
- client: AgentClient.create eagerly фетчит info (GET {baseUrl})
- client: HttpConversationStore.get читает /record (ConversationRecord,
а не ConversationSnapshot — рассинхрон типов)
- client: noReadTimeout() на POST /conversations/{id}/messages — сервер отвечает
по завершении всего хода агента (реально 0.5–144 с), дефолтные 15 с рвали
живую реплику на клиенте
- journal-api: ConversationRecord @Serializable
- ksqlite 0.1.3 → 0.1.4
- .gitignore: runtime-данные standalone-агента и hs_err-дампы
This commit is contained in:
@@ -28,6 +28,26 @@ interface Agent : AutoCloseable {
|
||||
/** Идентификатор агента. */
|
||||
val id: String
|
||||
|
||||
/**
|
||||
* Публичное представление имени/назначения агента — то, что агент
|
||||
* сам "рассказывает о себе" клиентам и UI.
|
||||
*
|
||||
* Контрактно отличается от [id]:
|
||||
* - [id] — opaque stable identifier (логи, корреляция, A2A contextId);
|
||||
* - [info.name] — **человекочитаемое** имя, которое пользователь
|
||||
* может сменить через конфиг или UI; может меняться в течение жизни
|
||||
* агента.
|
||||
*
|
||||
* Используется HTTP-фасадом `:server` (endpoint под `path` самого
|
||||
* агента) и A2A-фасадом (для заполнения AgentCard `name` / `description`).
|
||||
*
|
||||
* Доступ к [info] всегда дешёвый — это snapshot `data class` без побочных
|
||||
* эффектов. Конкретные реализации ([MutableAgent]) могут выставлять
|
||||
* `info` конструкторным параметром; [Agent] контрактно не требует
|
||||
* реактивности — клиенты получают снимок в момент обращения.
|
||||
*/
|
||||
val info: AgentInfo
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы агента (HTTP-клиент, сетевые handles, подписки).
|
||||
* После [close] вызовы [createConversation] / [getConversation] и т.п.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package pw.binom.agentik.proto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Идентификационная информация об [Agent] — публичное представление для
|
||||
* UI/A2A-агентов/IRC/визуальных дашбордов: имя ("как зовут ассистента"),
|
||||
* краткое описание ("что он делает") и зачем он может быть полезен
|
||||
* ("в каких задачах помогает").
|
||||
*
|
||||
* Контрактное отличие от [Agent.id]:
|
||||
* - [Agent.id] — стабильный opaque identifier для корреляции, логов и
|
||||
* транспорта (A2A `contextId`, IRC CTCP-запросы, etc). Менять нельзя.
|
||||
* - [AgentInfo.name] — **человекочитаемое имя**, которое показывается
|
||||
* клиенту и которое пользователь может настроить (например,
|
||||
* `AGENTIK_NAME` в standalone-конфиге). Может меняться.
|
||||
*
|
||||
* Wire-формат (через `:server` JSON-фасад): `name` — обязательное поле,
|
||||
* `description` и `usefulness` — опциональные, могут отсутствовать.
|
||||
*
|
||||
* @property name Человекочитаемое имя ассистента. Обязательное.
|
||||
* @property description Краткое описание (что агент делает/кто он).
|
||||
* Не сериализуется, если `null`.
|
||||
* @property usefulness Зачем агент может быть полезен. Не сериализуется,
|
||||
* если `null`.
|
||||
*/
|
||||
@Serializable
|
||||
data class AgentInfo(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
@SerialName("usefulness")
|
||||
val usefulness: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
package pw.binom.agentik.proto
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* Сериализация [AgentInfo] — контракт, по которому
|
||||
* `GET {path}` отдаёт JSON клиенту/A2A.
|
||||
*
|
||||
* JSON-форма `description` и `usefulness` исключаются, когда null
|
||||
* (благодаря `explicitNulls = false` в `agentikJson`).
|
||||
*/
|
||||
class AgentInfoSerializationTest {
|
||||
|
||||
private val json = Json {
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onlyName_whenOptionalsAreNull() {
|
||||
val info = AgentInfo(name = "agentik")
|
||||
val s = json.encodeToString(info)
|
||||
assertEquals("""{"name":"agentik"}""", s)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allFields_whenAllSet() {
|
||||
val info = AgentInfo(
|
||||
name = "Hermes",
|
||||
description = "Ассистент-разработчик на Kotlin",
|
||||
usefulness = "Помогает писать код, искать баги, объяснять legacy",
|
||||
)
|
||||
val s = json.encodeToString(info)
|
||||
assertEquals(
|
||||
"""{"name":"Hermes","description":"Ассистент-разработчик на Kotlin","usefulness":"Помогает писать код, искать баги, объяснять legacy"}""",
|
||||
s,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun descriptionOnly_usefulnessOmitted() {
|
||||
val info = AgentInfo(name = "x", description = "d")
|
||||
val s = json.encodeToString(info)
|
||||
assertEquals("""{"name":"x","description":"d"}""", s)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun usefulnessOnly_descriptionOmitted() {
|
||||
val info = AgentInfo(name = "x", usefulness = "u")
|
||||
val s = json.encodeToString(info)
|
||||
assertEquals("""{"name":"x","usefulness":"u"}""", s)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roundTrip_preservesFields() {
|
||||
val original = AgentInfo(name = "Hermes", description = "d", usefulness = "u")
|
||||
val decoded = json.decodeFromString<AgentInfo>(json.encodeToString(original))
|
||||
assertEquals(original, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultConstructor_optionalsAreNull() {
|
||||
val info = AgentInfo(name = "x")
|
||||
assertNull(info.description)
|
||||
assertNull(info.usefulness)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user