feat: add session info panel with message count, context usage, cache stats, and skills

Add GET /bots/:bot_id/sessions/:session_id/info API endpoint that returns
per-session message count, latest input token usage with model context window,
aggregated KV cache hit rate, and skills invoked via use_skill tool calls.
Frontend Info tab in the right sidebar now displays this data in a compact
key-value layout with a context usage progress bar and clickable skill links.
This commit is contained in:
Acbox
2026-04-02 03:17:28 +08:00
parent b308c27f74
commit b3c783fb0b
16 changed files with 898 additions and 11 deletions
+11 -1
View File
@@ -202,7 +202,17 @@
"scheduleDescription": "Description",
"schedulePattern": "Cron Pattern",
"scheduleMaxCalls": "Max Calls",
"viewSchedule": "View Schedule"
"viewSchedule": "View Schedule",
"infoMessages": "Messages",
"infoContextUsage": "Context Usage",
"infoContextTokens": "{used} / {window}",
"infoContextTokensNoWindow": "{used} / --",
"infoCacheHitRate": "Cache Hit Rate",
"infoCacheRead": "Cache Read",
"infoCacheWrite": "Cache Write",
"infoSkills": "Skills",
"infoNoSkills": "No skills used in this session",
"infoNoData": "No data available"
},
"models": {
"title": "Models",
+11 -1
View File
@@ -198,7 +198,17 @@
"scheduleDescription": "描述",
"schedulePattern": "Cron 表达式",
"scheduleMaxCalls": "最大调用次数",
"viewSchedule": "查看定时任务"
"viewSchedule": "查看定时任务",
"infoMessages": "消息数",
"infoContextUsage": "上下文使用率",
"infoContextTokens": "{used} / {window}",
"infoContextTokensNoWindow": "{used} / --",
"infoCacheHitRate": "Cache 命中率",
"infoCacheRead": "Cache 读取",
"infoCacheWrite": "Cache 写入",
"infoSkills": "Skills",
"infoNoSkills": "此会话未使用任何 Skill",
"infoNoData": "暂无数据"
},
"models": {
"title": "模型",
@@ -290,11 +290,12 @@
</div>
<div
v-if="activeRightTab === 'info'"
class="absolute inset-0 flex items-center justify-center"
class="absolute inset-0"
>
<p class="text-sm text-muted-foreground">
Info
</p>
<SessionInfoPanel
:visible="activeRightTab === 'info'"
:override-model-id="overrideModelId"
/>
</div>
</div>
</div>
@@ -376,6 +377,7 @@ import FileManager from '@/components/file-manager/index.vue'
import TerminalComponent from '@/components/terminal/index.vue'
import ModelOptions from '@/pages/bots/components/model-options.vue'
import ReasoningEffortSelect from '@/pages/bots/components/reasoning-effort-select.vue'
import SessionInfoPanel from './session-info-panel.vue'
import { EFFORT_LABELS, EFFORT_OPACITY } from '@/pages/bots/components/reasoning-effort'
import { useMediaGallery } from '../composables/useMediaGallery'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
@@ -0,0 +1,168 @@
<template>
<ScrollArea class="h-full">
<div class="px-4 py-3">
<!-- No session -->
<div
v-if="!sessionId"
class="flex items-center justify-center h-40"
>
<p class="text-xs text-muted-foreground">
{{ $t('chat.infoNoData') }}
</p>
</div>
<template v-else>
<!-- Key-value rows -->
<div class="divide-y divide-border text-xs">
<!-- Messages -->
<div class="flex items-center justify-between py-2">
<span class="text-muted-foreground">{{ $t('chat.infoMessages') }}</span>
<span class="font-medium text-foreground tabular-nums">{{ info?.message_count ?? '--' }}</span>
</div>
<!-- Context Usage -->
<div class="py-2 space-y-1.5">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">{{ $t('chat.infoContextUsage') }}</span>
<span class="font-medium text-foreground tabular-nums">
<template v-if="contextWindow != null">
{{ formatTokenCount(usedTokens) }} / {{ formatTokenCount(contextWindow) }}
<span class="text-muted-foreground font-normal ml-1">({{ contextPercent.toFixed(1) }}%)</span>
</template>
<template v-else>
{{ formatTokenCount(usedTokens) }} / --
</template>
</span>
</div>
<div
v-if="contextWindow != null && contextWindow > 0"
class="w-full h-1 rounded-full bg-accent overflow-hidden"
>
<div
class="h-full rounded-full transition-all"
:class="contextBarColor"
:style="{ width: `${Math.min(contextPercent, 100)}%` }"
/>
</div>
</div>
<!-- Cache Hit Rate -->
<div class="flex items-center justify-between py-2">
<span class="text-muted-foreground">{{ $t('chat.infoCacheHitRate') }}</span>
<span class="font-medium text-foreground tabular-nums">{{ cacheHitRate }}%</span>
</div>
<!-- Cache Read -->
<div class="flex items-center justify-between py-2">
<span class="text-muted-foreground">{{ $t('chat.infoCacheRead') }}</span>
<span class="font-medium text-foreground tabular-nums">{{ formatTokenCount(info?.cache_stats?.cache_read_tokens ?? 0) }}</span>
</div>
<!-- Cache Write -->
<div class="flex items-center justify-between py-2">
<span class="text-muted-foreground">{{ $t('chat.infoCacheWrite') }}</span>
<span class="font-medium text-foreground tabular-nums">{{ formatTokenCount(info?.cache_stats?.cache_write_tokens ?? 0) }}</span>
</div>
</div>
<!-- Skills -->
<div class="mt-3">
<p class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
{{ $t('chat.infoSkills') }}
</p>
<div
v-if="!skills.length"
class="text-xs text-muted-foreground"
>
{{ $t('chat.infoNoSkills') }}
</div>
<div
v-else
class="space-y-0.5"
>
<button
v-for="skill in skills"
:key="skill"
type="button"
class="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-foreground hover:bg-accent transition-colors text-left"
@click="openSkillFile(skill)"
>
<Sparkles class="size-3 text-muted-foreground shrink-0" />
<span class="truncate">{{ skill }}</span>
<ExternalLink class="size-3 text-muted-foreground shrink-0 ml-auto" />
</button>
</div>
</div>
</template>
</div>
</ScrollArea>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { storeToRefs } from 'pinia'
import { useQuery } from '@pinia/colada'
import { Sparkles, ExternalLink } from 'lucide-vue-next'
import { ScrollArea } from '@memohai/ui'
import { getBotsByBotIdSessionsBySessionIdInfo } from '@memohai/sdk'
import type { HandlersSessionInfoResponse } from '@memohai/sdk'
import { useChatStore } from '@/store/chat-list'
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
const props = defineProps<{
visible: boolean
overrideModelId?: string
}>()
const chatStore = useChatStore()
const { currentBotId, sessionId } = storeToRefs(chatStore)
const openInFileManager = inject(openInFileManagerKey, undefined)
const { data: info } = useQuery({
key: () => ['session-info', currentBotId.value ?? '', sessionId.value ?? '', props.overrideModelId ?? ''],
query: async () => {
const { data } = await getBotsByBotIdSessionsBySessionIdInfo({
path: {
bot_id: currentBotId.value!,
session_id: sessionId.value!,
},
query: {
model_id: props.overrideModelId || undefined,
},
throwOnError: true,
})
return data as HandlersSessionInfoResponse
},
enabled: () => !!currentBotId.value && !!sessionId.value && props.visible,
refetchOnWindowFocus: false,
})
const usedTokens = computed(() => info.value?.context_usage?.used_tokens ?? 0)
const contextWindow = computed(() => info.value?.context_usage?.context_window ?? null)
const contextPercent = computed(() => {
if (contextWindow.value == null || contextWindow.value <= 0) return 0
return (usedTokens.value / contextWindow.value) * 100
})
const contextBarColor = computed(() => {
if (contextPercent.value >= 90) return 'bg-destructive'
if (contextPercent.value >= 70) return 'bg-amber-500'
return 'bg-foreground'
})
const cacheHitRate = computed(() => {
const rate = info.value?.cache_stats?.cache_hit_rate ?? 0
return rate.toFixed(1)
})
const skills = computed(() => info.value?.skills ?? [])
function formatTokenCount(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n)
}
function openSkillFile(skillName: string) {
openInFileManager?.(`/data/skills/${skillName}/SKILL.md`, false)
}
</script>
+1
View File
@@ -256,6 +256,7 @@ func runServe() {
provideServerHandler(handlers.NewMCPOAuthHandler),
provideOAuthService,
provideServerHandler(handlers.NewTokenUsageHandler),
provideServerHandler(handlers.NewSessionInfoHandler),
provideServerHandler(handlers.NewBrowserContextsHandler),
provideServerHandler(handlers.NewSupermarketHandler),
provideServerHandler(provideCLIHandler),
+1
View File
@@ -183,6 +183,7 @@ func runServe() {
provideServerHandler(handlers.NewMCPOAuthHandler),
provideOAuthService,
provideServerHandler(handlers.NewTokenUsageHandler),
provideServerHandler(handlers.NewSessionInfoHandler),
provideServerHandler(handlers.NewBrowserContextsHandler),
provideServerHandler(provideCLIHandler),
provideServerHandler(provideWebHandler),
+41
View File
@@ -0,0 +1,41 @@
-- name: CountMessagesBySession :one
SELECT COUNT(*)::bigint AS message_count
FROM bot_history_messages
WHERE session_id = sqlc.arg(session_id);
-- name: GetLatestAssistantUsage :one
SELECT
COALESCE((m.usage->>'inputTokens')::bigint, 0)::bigint AS input_tokens
FROM bot_history_messages m
WHERE m.session_id = sqlc.arg(session_id)
AND m.role = 'assistant'
AND m.usage IS NOT NULL
ORDER BY m.created_at DESC
LIMIT 1;
-- name: GetSessionCacheStats :one
SELECT
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS total_input_tokens,
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheReadTokens')::bigint), 0)::bigint AS cache_read_tokens,
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens
FROM bot_history_messages m
WHERE m.session_id = sqlc.arg(session_id)
AND m.usage IS NOT NULL;
-- name: GetSessionUsedSkills :many
SELECT DISTINCT
(part->'input'->>'skillName')::text AS skill_name
FROM bot_history_messages m,
jsonb_array_elements(
CASE WHEN jsonb_typeof(m.content->'content') = 'array'
THEN m.content->'content'
ELSE '[]'::jsonb
END
) AS part
WHERE m.session_id = sqlc.arg(session_id)
AND m.role = 'assistant'
AND part->>'type' = 'tool-call'
AND part->>'toolName' = 'use_skill'
AND part->'input'->>'skillName' IS NOT NULL
AND part->'input'->>'skillName' != ''
ORDER BY skill_name;
+105
View File
@@ -0,0 +1,105 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: session_info.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countMessagesBySession = `-- name: CountMessagesBySession :one
SELECT COUNT(*)::bigint AS message_count
FROM bot_history_messages
WHERE session_id = $1
`
func (q *Queries) CountMessagesBySession(ctx context.Context, sessionID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countMessagesBySession, sessionID)
var message_count int64
err := row.Scan(&message_count)
return message_count, err
}
const getLatestAssistantUsage = `-- name: GetLatestAssistantUsage :one
SELECT
COALESCE((m.usage->>'inputTokens')::bigint, 0)::bigint AS input_tokens
FROM bot_history_messages m
WHERE m.session_id = $1
AND m.role = 'assistant'
AND m.usage IS NOT NULL
ORDER BY m.created_at DESC
LIMIT 1
`
func (q *Queries) GetLatestAssistantUsage(ctx context.Context, sessionID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, getLatestAssistantUsage, sessionID)
var input_tokens int64
err := row.Scan(&input_tokens)
return input_tokens, err
}
const getSessionCacheStats = `-- name: GetSessionCacheStats :one
SELECT
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS total_input_tokens,
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheReadTokens')::bigint), 0)::bigint AS cache_read_tokens,
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens
FROM bot_history_messages m
WHERE m.session_id = $1
AND m.usage IS NOT NULL
`
type GetSessionCacheStatsRow struct {
TotalInputTokens int64 `json:"total_input_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWriteTokens int64 `json:"cache_write_tokens"`
}
func (q *Queries) GetSessionCacheStats(ctx context.Context, sessionID pgtype.UUID) (GetSessionCacheStatsRow, error) {
row := q.db.QueryRow(ctx, getSessionCacheStats, sessionID)
var i GetSessionCacheStatsRow
err := row.Scan(&i.TotalInputTokens, &i.CacheReadTokens, &i.CacheWriteTokens)
return i, err
}
const getSessionUsedSkills = `-- name: GetSessionUsedSkills :many
SELECT DISTINCT
(part->'input'->>'skillName')::text AS skill_name
FROM bot_history_messages m,
jsonb_array_elements(
CASE WHEN jsonb_typeof(m.content->'content') = 'array'
THEN m.content->'content'
ELSE '[]'::jsonb
END
) AS part
WHERE m.session_id = $1
AND m.role = 'assistant'
AND part->>'type' = 'tool-call'
AND part->>'toolName' = 'use_skill'
AND part->'input'->>'skillName' IS NOT NULL
AND part->'input'->>'skillName' != ''
ORDER BY skill_name
`
func (q *Queries) GetSessionUsedSkills(ctx context.Context, sessionID pgtype.UUID) ([]string, error) {
rows, err := q.db.Query(ctx, getSessionUsedSkills, sessionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var skill_name string
if err := rows.Scan(&skill_name); err != nil {
return nil, err
}
items = append(items, skill_name)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
+177
View File
@@ -0,0 +1,177 @@
package handlers
import (
"errors"
"log/slog"
"net/http"
"strings"
"github.com/jackc/pgx/v5"
"github.com/labstack/echo/v4"
"github.com/memohai/memoh/internal/accounts"
"github.com/memohai/memoh/internal/bots"
"github.com/memohai/memoh/internal/db"
"github.com/memohai/memoh/internal/db/sqlc"
"github.com/memohai/memoh/internal/models"
"github.com/memohai/memoh/internal/settings"
)
type SessionInfoHandler struct {
queries *sqlc.Queries
botService *bots.Service
accountService *accounts.Service
modelsService *models.Service
settingsService *settings.Service
logger *slog.Logger
}
func NewSessionInfoHandler(log *slog.Logger, queries *sqlc.Queries, botService *bots.Service, accountService *accounts.Service, modelsService *models.Service, settingsService *settings.Service) *SessionInfoHandler {
return &SessionInfoHandler{
queries: queries,
botService: botService,
accountService: accountService,
modelsService: modelsService,
settingsService: settingsService,
logger: log.With(slog.String("handler", "session_info")),
}
}
func (h *SessionInfoHandler) Register(e *echo.Echo) {
e.GET("/bots/:bot_id/sessions/:session_id/info", h.GetSessionInfo)
}
type SessionInfoResponse struct {
MessageCount int64 `json:"message_count"`
ContextUsage ContextUsage `json:"context_usage"`
CacheStats CacheStats `json:"cache_stats"`
Skills []string `json:"skills"`
}
type ContextUsage struct {
UsedTokens int64 `json:"used_tokens"`
ContextWindow *int64 `json:"context_window,omitempty"`
}
type CacheStats struct {
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWriteTokens int64 `json:"cache_write_tokens"`
TotalInputTokens int64 `json:"total_input_tokens"`
CacheHitRate float64 `json:"cache_hit_rate"`
}
// GetSessionInfo godoc
// @Summary Get session info
// @Description Get aggregated info for a chat session including message count, context usage, cache stats, and used skills
// @Tags sessions
// @Param bot_id path string true "Bot ID"
// @Param session_id path string true "Session ID"
// @Param model_id query string false "Optional model UUID override for context window"
// @Success 200 {object} SessionInfoResponse
// @Failure 400 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /bots/{bot_id}/sessions/{session_id}/info [get].
func (h *SessionInfoHandler) GetSessionInfo(c echo.Context) error {
userID, err := RequireChannelIdentityID(c)
if err != nil {
return err
}
botID := strings.TrimSpace(c.Param("bot_id"))
if botID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "bot id is required")
}
if _, err := AuthorizeBotAccess(c.Request().Context(), h.botService, h.accountService, userID, botID); err != nil {
return err
}
sessionID := strings.TrimSpace(c.Param("session_id"))
if sessionID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "session id is required")
}
pgSessionID, err := db.ParseUUID(sessionID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid session id")
}
ctx := c.Request().Context()
messageCount, err := h.queries.CountMessagesBySession(ctx, pgSessionID)
if err != nil {
h.logger.Error("count messages failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to count messages")
}
var usedTokens int64
latestUsage, err := h.queries.GetLatestAssistantUsage(ctx, pgSessionID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
h.logger.Error("get latest usage failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get latest usage")
}
if err == nil {
usedTokens = latestUsage
}
contextWindow := h.resolveContextWindow(c, botID)
cacheRow, err := h.queries.GetSessionCacheStats(ctx, pgSessionID)
if err != nil {
h.logger.Error("get cache stats failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get cache stats")
}
var cacheHitRate float64
if cacheRow.TotalInputTokens > 0 {
cacheHitRate = float64(cacheRow.CacheReadTokens) / float64(cacheRow.TotalInputTokens) * 100
}
skills, err := h.queries.GetSessionUsedSkills(ctx, pgSessionID)
if err != nil {
h.logger.Error("get used skills failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get used skills")
}
if skills == nil {
skills = []string{}
}
resp := SessionInfoResponse{
MessageCount: messageCount,
ContextUsage: ContextUsage{
UsedTokens: usedTokens,
ContextWindow: contextWindow,
},
CacheStats: CacheStats{
CacheReadTokens: cacheRow.CacheReadTokens,
CacheWriteTokens: cacheRow.CacheWriteTokens,
TotalInputTokens: cacheRow.TotalInputTokens,
CacheHitRate: cacheHitRate,
},
Skills: skills,
}
return c.JSON(http.StatusOK, resp)
}
func (h *SessionInfoHandler) resolveContextWindow(c echo.Context, botID string) *int64 {
modelIDStr := strings.TrimSpace(c.QueryParam("model_id"))
if modelIDStr == "" && h.settingsService != nil {
s, err := h.settingsService.GetBot(c.Request().Context(), botID)
if err == nil && s.ChatModelID != "" {
modelIDStr = s.ChatModelID
}
}
if modelIDStr == "" || h.modelsService == nil {
return nil
}
m, err := h.modelsService.GetByID(c.Request().Context(), modelIDStr)
if err != nil {
return nil
}
if m.Config.ContextWindow == nil {
return nil
}
cw := int64(*m.Config.ContextWindow)
return &cw
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+66
View File
@@ -719,6 +719,13 @@ export type HandlersBrowserCoresResponse = {
cores?: Array<string>;
};
export type HandlersCacheStats = {
cache_hit_rate?: number;
cache_read_tokens?: number;
cache_write_tokens?: number;
total_input_tokens?: number;
};
export type HandlersChannelMeta = {
capabilities?: ChannelChannelCapabilities;
config_schema?: ChannelConfigSchema;
@@ -729,6 +736,11 @@ export type HandlersChannelMeta = {
user_config_schema?: ChannelConfigSchema;
};
export type HandlersContextUsage = {
context_window?: number;
used_tokens?: number;
};
export type HandlersCreateContainerRequest = {
image?: string;
restore_data?: boolean;
@@ -916,6 +928,13 @@ export type HandlersRollbackRequest = {
version?: number;
};
export type HandlersSessionInfoResponse = {
cache_stats?: HandlersCacheStats;
context_usage?: HandlersContextUsage;
message_count?: number;
skills?: Array<string>;
};
export type HandlersSkillItem = {
content?: string;
description?: string;
@@ -5237,6 +5256,53 @@ export type PatchBotsByBotIdSessionsBySessionIdResponses = {
export type PatchBotsByBotIdSessionsBySessionIdResponse = PatchBotsByBotIdSessionsBySessionIdResponses[keyof PatchBotsByBotIdSessionsBySessionIdResponses];
export type GetBotsByBotIdSessionsBySessionIdInfoData = {
body?: never;
path: {
/**
* Bot ID
*/
bot_id: string;
/**
* Session ID
*/
session_id: string;
};
query?: {
/**
* Optional model UUID override for context window
*/
model_id?: string;
};
url: '/bots/{bot_id}/sessions/{session_id}/info';
};
export type GetBotsByBotIdSessionsBySessionIdInfoErrors = {
/**
* Bad Request
*/
400: HandlersErrorResponse;
/**
* Forbidden
*/
403: HandlersErrorResponse;
/**
* Internal Server Error
*/
500: HandlersErrorResponse;
};
export type GetBotsByBotIdSessionsBySessionIdInfoError = GetBotsByBotIdSessionsBySessionIdInfoErrors[keyof GetBotsByBotIdSessionsBySessionIdInfoErrors];
export type GetBotsByBotIdSessionsBySessionIdInfoResponses = {
/**
* OK
*/
200: HandlersSessionInfoResponse;
};
export type GetBotsByBotIdSessionsBySessionIdInfoResponse = GetBotsByBotIdSessionsBySessionIdInfoResponses[keyof GetBotsByBotIdSessionsBySessionIdInfoResponses];
export type DeleteBotsByBotIdSettingsData = {
body?: never;
path?: never;
+105
View File
@@ -4395,6 +4395,63 @@ const docTemplate = `{
}
}
},
"/bots/{bot_id}/sessions/{session_id}/info": {
"get": {
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
"tags": [
"sessions"
],
"summary": "Get session info",
"parameters": [
{
"type": "string",
"description": "Bot ID",
"name": "bot_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Optional model UUID override for context window",
"name": "model_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SessionInfoResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/bots/{bot_id}/settings": {
"get": {
"description": "Get agent settings for current user",
@@ -10875,6 +10932,23 @@ const docTemplate = `{
}
}
},
"handlers.CacheStats": {
"type": "object",
"properties": {
"cache_hit_rate": {
"type": "number"
},
"cache_read_tokens": {
"type": "integer"
},
"cache_write_tokens": {
"type": "integer"
},
"total_input_tokens": {
"type": "integer"
}
}
},
"handlers.ChannelMeta": {
"type": "object",
"properties": {
@@ -10901,6 +10975,17 @@ const docTemplate = `{
}
}
},
"handlers.ContextUsage": {
"type": "object",
"properties": {
"context_window": {
"type": "integer"
},
"used_tokens": {
"type": "integer"
}
}
},
"handlers.CreateContainerRequest": {
"type": "object",
"properties": {
@@ -11359,6 +11444,26 @@ const docTemplate = `{
}
}
},
"handlers.SessionInfoResponse": {
"type": "object",
"properties": {
"cache_stats": {
"$ref": "#/definitions/handlers.CacheStats"
},
"context_usage": {
"$ref": "#/definitions/handlers.ContextUsage"
},
"message_count": {
"type": "integer"
},
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"handlers.SkillItem": {
"type": "object",
"properties": {
+105
View File
@@ -4386,6 +4386,63 @@
}
}
},
"/bots/{bot_id}/sessions/{session_id}/info": {
"get": {
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
"tags": [
"sessions"
],
"summary": "Get session info",
"parameters": [
{
"type": "string",
"description": "Bot ID",
"name": "bot_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Session ID",
"name": "session_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Optional model UUID override for context window",
"name": "model_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.SessionInfoResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/bots/{bot_id}/settings": {
"get": {
"description": "Get agent settings for current user",
@@ -10866,6 +10923,23 @@
}
}
},
"handlers.CacheStats": {
"type": "object",
"properties": {
"cache_hit_rate": {
"type": "number"
},
"cache_read_tokens": {
"type": "integer"
},
"cache_write_tokens": {
"type": "integer"
},
"total_input_tokens": {
"type": "integer"
}
}
},
"handlers.ChannelMeta": {
"type": "object",
"properties": {
@@ -10892,6 +10966,17 @@
}
}
},
"handlers.ContextUsage": {
"type": "object",
"properties": {
"context_window": {
"type": "integer"
},
"used_tokens": {
"type": "integer"
}
}
},
"handlers.CreateContainerRequest": {
"type": "object",
"properties": {
@@ -11350,6 +11435,26 @@
}
}
},
"handlers.SessionInfoResponse": {
"type": "object",
"properties": {
"cache_stats": {
"$ref": "#/definitions/handlers.CacheStats"
},
"context_usage": {
"$ref": "#/definitions/handlers.ContextUsage"
},
"message_count": {
"type": "integer"
},
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"handlers.SkillItem": {
"type": "object",
"properties": {
+70
View File
@@ -1190,6 +1190,17 @@ definitions:
type: string
type: array
type: object
handlers.CacheStats:
properties:
cache_hit_rate:
type: number
cache_read_tokens:
type: integer
cache_write_tokens:
type: integer
total_input_tokens:
type: integer
type: object
handlers.ChannelMeta:
properties:
capabilities:
@@ -1207,6 +1218,13 @@ definitions:
user_config_schema:
$ref: '#/definitions/channel.ConfigSchema'
type: object
handlers.ContextUsage:
properties:
context_window:
type: integer
used_tokens:
type: integer
type: object
handlers.CreateContainerRequest:
properties:
image:
@@ -1503,6 +1521,19 @@ definitions:
version:
type: integer
type: object
handlers.SessionInfoResponse:
properties:
cache_stats:
$ref: '#/definitions/handlers.CacheStats'
context_usage:
$ref: '#/definitions/handlers.ContextUsage'
message_count:
type: integer
skills:
items:
type: string
type: array
type: object
handlers.SkillItem:
properties:
content:
@@ -5655,6 +5686,45 @@ paths:
summary: Update a session
tags:
- sessions
/bots/{bot_id}/sessions/{session_id}/info:
get:
description: Get aggregated info for a chat session including message count,
context usage, cache stats, and used skills
parameters:
- description: Bot ID
in: path
name: bot_id
required: true
type: string
- description: Session ID
in: path
name: session_id
required: true
type: string
- description: Optional model UUID override for context window
in: query
name: model_id
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.SessionInfoResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get session info
tags:
- sessions
/bots/{bot_id}/settings:
delete:
description: Remove agent settings for current user