From b83d1d1e9cf976aa4ce03f7aef12744668acdcb1 Mon Sep 17 00:00:00 2001
From: Leonid Pershin
Date: Sun, 23 Aug 2026 07:32:58 +0300
Subject: [PATCH] Add multi-turn Assistent session diagnostics to the Debug
API.
POST /assistent/session + /chat with rich per-turn traces (patch, Exact merge, compact_context); chat-eval wraps one session turn. Docs playbook and unit/HTTP tests included.
Co-authored-by: Cursor
---
docs/cli.md | 48 +-
src/gpu_rent/debug_api.py | 330 ++++++-
src/gpu_rent/debug_assistent.py | 273 +-----
src/gpu_rent/debug_assistent_session.py | 1163 +++++++++++++++++++++++
tests/test_assistent_session.py | 419 ++++++++
tests/test_debug_api.py | 24 +-
6 files changed, 1965 insertions(+), 292 deletions(-)
create mode 100644 src/gpu_rent/debug_assistent_session.py
create mode 100644 tests/test_assistent_session.py
diff --git a/docs/cli.md b/docs/cli.md
index 9144c65..d878ebd 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -133,31 +133,59 @@ Exit 0 → можно `up`. Exit 1 → причина в таблице / кра
| Путь | Зачем |
| --- | --- |
| `/assistent` | Сводка: extension + overlay + roles + memory + live API + hints + playbook |
+| `/assistent/diagnose` | То же + journal по умолчанию + session store meta |
| `/assistent/extension` | DLL, Sqlite deps, Tab/Assets, git HEAD |
| `/assistent/overlay` | personas на VM vs локальный `assistent-personas/` |
| `/assistent/roles` | `ollama-roles.json` ↔ `/api/tags` (`default_chat`) |
| `/assistent/memory` | `assistent.sqlite` + counts |
| `/assistent/api` | `AssistentListPersonas/Models/Memory/Chats` через туннель (или SSH) |
| `/assistent/api?chat_smoke=1` | + 1-token Ollama `/api/chat` (кратко грузит модель) |
-| `/assistent/chat-eval` | **Opt-in** `GetNewSession` → `AssistentChat` (полный ответ ассистента) |
| `/assistent/logs` | journalctl swarmui: build/load/Sqlite |
| `/assistent?logs=1` | Сводка + journal |
+| `/assistent/session` | **POST** — multi-turn debug chat (in-memory, TTL ~45m) |
+| `/assistent/session/{id}/chat` | **POST** — ход + объект `trace` |
+| `/assistent/session/{id}` | **GET** / **DELETE** — состояние / сброс |
+| `/assistent/chat-eval` | One-shot: session + один chat + delete |
Симптомы → куда смотреть: поле `playbook` в `/assistent`.
-**`/assistent/chat-eval`** — не входит в `/snapshot`. Может загрузить chat-модель в VRAM и записать чат в Sqlite (если DLL на месте). Предпочтительно `POST` с JSON; для агентов удобен и `GET` с query.
+**Session / chat-eval** не входят в `/snapshot`. Могут грузить VRAM, писать sqlite chat, тратить биллинг GPU. `compactContext` / Exact merge в Assistent HTTP API **нет** — sidecar реконструирует best-effort (`trace.gaps`).
+
+#### Agent playbook (curl)
```bash
-# POST (предпочтительно)
-curl -sS -X POST http://127.0.0.1:17821/assistent/chat-eval \
- -H "Content-Type: application/json" \
- -d "{\"message\":\"какой checkpoint и какие steps/cfg должны быть\",\"persona\":\"leonid\",\"timeout\":120}"
+BASE=http://127.0.0.1:17821
-# GET (агенты / быстрый smoke)
-curl -sS "http://127.0.0.1:17821/assistent/chat-eval?message=ping&timeout=90"
+# Static+live health (no chat)
+curl -sS "$BASE/assistent/diagnose" | jq '{ok,hints,playbook,session_store}'
+
+# Multi-turn conversational diagnostics
+SID=$(curl -sS -X POST "$BASE/assistent/session" \
+ -H 'Content-Type: application/json' \
+ -d '{"persona":"neutral","pack":"ordinary","context":{"krea_profile":"turbo","checkpoint":"krea-turbo"}}' \
+ | jq -r .debug_session_id)
+
+curl -sS -X POST "$BASE/assistent/session/$SID/chat" \
+ -H 'Content-Type: application/json' \
+ -d '{"message":"какой checkpoint и какие steps/cfg должны быть"}' \
+ | jq '{ok,reply,trace:{timings_ms,patch,exact_merge,compact_context,system_layers,gaps}}'
+
+curl -sS -X POST "$BASE/assistent/session/$SID/chat" \
+ -H 'Content-Type: application/json' \
+ -d '{"message":"сделай generate с этими params"}' \
+ | jq '{ok, trace:{patch,exact_merge}}'
+
+curl -sS "$BASE/assistent/session/$SID" | jq .session
+curl -sS -X DELETE "$BASE/assistent/session/$SID"
+
+# One-shot convenience (session + one chat + delete)
+curl -sS -X POST "$BASE/assistent/chat-eval" \
+ -H 'Content-Type: application/json' \
+ -d '{"message":"какие steps/cfg для turbo?","persona":"leonid","timeout":120}' \
+ | jq '{ok,reply,patch,trace}'
```
-Ответ: `ok`, `ms` / `chat_ms`, `model` / `preferred`, `reply` / `reply_prose`, `patch` (prompt/steps/cfg/aspect/actions…), `errors`, `hints`. Туннель `:swarmui_local_port` (обычно 17801), иначе SSH на VM `:7801`.
+Ответ chat: `ok`, `reply`, `trace` (`timings_ms`, `patch`, `exact_merge`, `compact_context`, `system_chars` / `system_layers`, `skills`, `hops`, `gaps`, `errors`/`warnings`/`hints`). Sqlite SaveChat fail — soft error, если текст ответа есть.
После `up --no-tunnel` процесс завершается и sidecar гаснет — держи отдельно:
@@ -165,7 +193,7 @@ curl -sS "http://127.0.0.1:17821/assistent/chat-eval?message=ping&timeout=90"
gpu-rent debug
```
-Мутаций конфига/GPU нет (restart/hold/stop — как раньше через CLI). `chat_smoke` только пингует Ollama; `chat-eval` — полноценный AssistentChat (opt-in).
+Мутаций конфига/GPU нет (restart/hold/stop — как раньше через CLI). `chat_smoke` только пингует Ollama; session/chat-eval — полноценный AssistentChat (opt-in).
---
diff --git a/src/gpu_rent/debug_api.py b/src/gpu_rent/debug_api.py
index 3a1ad81..c980fc4 100644
--- a/src/gpu_rent/debug_api.py
+++ b/src/gpu_rent/debug_api.py
@@ -1,8 +1,8 @@
"""Localhost debug HTTP sidecar for gpu-rent.
Bind only 127.0.0.1. Started from `up` / `tunnel` / `debug`.
-Mostly read-only; `/assistent/chat-eval` is an opt-in AssistentChat probe
-(may load VRAM / persist chat) and is not part of `/snapshot`.
+Mostly read-only; Assistent session / chat-eval endpoints are opt-in
+(may load VRAM / persist chat) and are not part of `/snapshot`.
"""
from __future__ import annotations
@@ -63,16 +63,25 @@ _CHAT_EVAL_PARAMS = [
},
]
+_WARN_VRAM = (
+ "WARNING: may load chat model into VRAM, may write assistent.sqlite chat, "
+ "and incurs GPU billing if the VM is up. Not part of /snapshot. "
+ "compactContext / Exact merge are reconstructed client-side "
+ "(Assistent HTTP API does not return them)."
+)
+
_OPENAPI: dict[str, Any] = {
"openapi": "3.0.3",
"info": {
"title": "gpu-rent Debug API",
- "version": "1.1.0",
+ "version": "1.2.0",
"description": (
"Localhost diagnostics for installer progress and "
- "SwarmUI / Comfy / Ollama / Assistent. Mostly read-only; "
- "POST/GET /assistent/chat-eval is opt-in AssistentChat "
- "(may load VRAM / write chat) and is skipped from /snapshot."
+ "SwarmUI / Comfy / Ollama / Assistent. Mostly read-only. "
+ "Assistent multi-turn: POST /assistent/session + "
+ "/assistent/session/{id}/chat (trace object). "
+ "One-shot: POST/GET /assistent/chat-eval. "
+ + _WARN_VRAM
),
},
"servers": [{"url": "http://127.0.0.1:17821"}],
@@ -95,7 +104,9 @@ _OPENAPI: dict[str, Any] = {
}
},
"/snapshot": {
- "get": {"summary": "Cheap aggregate (no full diag, no chat-eval)"}
+ "get": {
+ "summary": "Cheap aggregate (no full diag, no Assistent chat/session)"
+ }
},
"/status": {"get": {"summary": "JSON mirror of gpu-rent status"}},
"/state": {"get": {"summary": "state.json without secrets"}},
@@ -142,6 +153,27 @@ _OPENAPI: dict[str, Any] = {
],
}
},
+ "/assistent/diagnose": {
+ "get": {
+ "summary": (
+ "Full static+live Assistent health (logs on by default). "
+ + _WARN_VRAM
+ ),
+ "parameters": [
+ {
+ "name": "chat_smoke",
+ "in": "query",
+ "schema": {"type": "boolean"},
+ },
+ {
+ "name": "logs",
+ "in": "query",
+ "schema": {"type": "boolean", "default": True},
+ "description": "Pass logs=0 to skip journal",
+ },
+ ],
+ }
+ },
"/assistent/extension": {
"get": {"summary": "DLL/Sqlite deps/Tab assets under Extensions/swarm-assistent"}
},
@@ -171,18 +203,92 @@ _OPENAPI: dict[str, Any] = {
"/assistent/logs": {
"get": {"summary": "journalctl swarmui filtered for Assistent/Sqlite/build"}
},
+ "/assistent/session": {
+ "post": {
+ "summary": (
+ "Start multi-turn Assistent debug session "
+ "(GetNewSession + optional GetConfig). " + _WARN_VRAM
+ ),
+ "requestBody": {
+ "required": False,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "persona": {"type": "string"},
+ "pack": {"type": "string"},
+ "model": {"type": "string"},
+ "skills": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "context": {
+ "type": "object",
+ "description": (
+ "Synthetic compactContext fields "
+ "(krea_profile, checkpoint, "
+ "recommended_params, …)"
+ ),
+ },
+ "probe_config": {
+ "type": "boolean",
+ "default": True,
+ },
+ },
+ }
+ }
+ },
+ },
+ }
+ },
+ "/assistent/session/{id}": {
+ "get": {"summary": "In-memory session summary + last trace"},
+ "delete": {"summary": "Drop in-memory debug session"},
+ },
+ "/assistent/session/{id}/chat": {
+ "post": {
+ "summary": (
+ "Send user message; returns reply + trace "
+ "(patch, Exact merge, compact_context sizes, system_layers). "
+ + _WARN_VRAM
+ ),
+ "requestBody": {
+ "required": True,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["message"],
+ "properties": {
+ "message": {"type": "string"},
+ "timeout": {"type": "number"},
+ "persona": {"type": "string"},
+ "pack": {"type": "string"},
+ "model": {"type": "string"},
+ "skills": {
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "context": {"type": "object"},
+ },
+ }
+ }
+ },
+ },
+ }
+ },
"/assistent/chat-eval": {
"get": {
"summary": (
- "Opt-in AssistentChat eval (GetNewSession→AssistentChat); "
- "may load VRAM / write chat — not in /snapshot"
+ "One-shot AssistentChat (session+chat+delete). " + _WARN_VRAM
),
"parameters": _CHAT_EVAL_PARAMS,
},
"post": {
"summary": (
"Same as GET; prefer JSON body "
- "{message,persona,pack,model,timeout}"
+ "{message,persona,pack,model,timeout,context}"
),
"requestBody": {
"required": False,
@@ -196,6 +302,7 @@ _OPENAPI: dict[str, Any] = {
"pack": {"type": "string"},
"model": {"type": "string"},
"timeout": {"type": "number"},
+ "context": {"type": "object"},
},
}
}
@@ -207,6 +314,21 @@ _OPENAPI: dict[str, Any] = {
}
+def _is_assistent_session_chat(path: str) -> bool:
+ parts = [p for p in path.split("/") if p]
+ return (
+ len(parts) == 4
+ and parts[0] == "assistent"
+ and parts[1] == "session"
+ and parts[3] == "chat"
+ )
+
+
+def _is_assistent_session_id(path: str) -> bool:
+ parts = [p for p in path.split("/") if p]
+ return len(parts) == 3 and parts[0] == "assistent" and parts[1] == "session"
+
+
@dataclass
class DebugHub:
"""Shared state for the sidecar process."""
@@ -397,7 +519,9 @@ def _make_handler(hub: DebugHub):
f"base {hub.base_url} · mostly read-only · 127.0.0.1
"
f"Agent: start at /openapi.json "
f"then /snapshot. "
- f"Opt-in chat: /assistent/chat-eval (not in snapshot).
"
+ f"Opt-in Assistent: /assistent/session (multi-turn) · "
+ f"/assistent/chat-eval (one-shot) — not in snapshot; "
+ f"may load VRAM.
"
f"