FrostAgent is structured as a layered Go application where each package has a single, well-defined responsibility. Incoming messages from a OneBot v11 WebSocket client travel through an adapter layer into the Engine’s agentic loop, which can invoke tools, maintain per-user session history, and deliver replies back through the same connection — all without coupling the LLM logic to any specific messaging platform.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/GuaiZai233/FrostAgent/llms.txt
Use this file to discover all available pages before exploring further.
High-Level Message Flow
When a user sends a message to the bot, the following sequence unfolds:OneBot WebSocket receives the event
The
internal/adapter/onebot WebSocket server upgrades the HTTP connection and reads a raw OneBot v11 JSON event. Each event is dispatched to a new goroutine via go processEvent(...) so the read loop is never blocked.Message is parsed and a session ID is resolved
processEvent extracts the user text, builds an implicit context map (user ID, group ID, message type), and computes a session key: group:<id> for group chats or private:<id> for DMs.Session history is loaded
The adapter calls
engine.SessionManager.GetOrCreate(sessionID) and then session.Snapshot() to obtain a thread-safe deep copy of the conversation history.Engine.RunMessages drives the agentic loop
The history snapshot is passed to
engine.RunMessages(messages), which calls the LLM provider in a loop of up to MaxIterations rounds, executing any requested tool calls and appending results to the message chain.SendHook delivers mid-turn messages immediately
When the
send_message tool fires during the loop, the Engine calls SendHook(toolResultJSON), which the adapter uses to write a fully-formed OneBot action frame to the WebSocket connection without waiting for the loop to finish.Core Packages
internal/core
The contract layer. Defines all shared interfaces (
LLMProvider, AgentService, MessageAdapter, MessageDispatcher, ToolRegistry, Session, SessionStore) and data types (ChatMessage, Tool, ToolCall, ChatRequest, ChatResponse, IncomingMessage, OutgoingMessage). No package in internal/core depends on any other internal package, making it the stable foundation everything else imports.internal/llm
The orchestration layer. Contains
Engine (the agentic loop), SessionManager / SessionContext (per-user history), and context utilities (TrimMessages, ApproxTokenCount). This is where tool calls are dispatched and LLM responses are interpreted.internal/adapter/onebot
The platform adapter. Implements a OneBot v11–compatible WebSocket server using
gorilla/websocket. It upgrades connections, parses message segments (text, image, at, face, file, …), performs image description via the LLM vision API, and serialises replies back as OneBot action frames.internal/tools
Built-in tool implementations. Each tool is a
tools.Tool struct with Name, Description, Parameters (JSON Schema), and an Execute closure. The four built-in tools are send_message, use_subagent, get_weather, and get_game_version.internal/provider/llm/openai
The LLM provider. Implements
core.LLMProvider against any OpenAI-compatible HTTP API. Configured at startup with UPSTREAM_ENDPOINT, UPSTREAM_API_KEY, and MODEL_NAME environment variables.internal/service/*
ConnectRPC gRPC-over-HTTP/2 services exposed on the management port (default
:8080). Includes BotStatusService (engine overview), SettingsService (.env read/write), and LogService (streaming log tail). The frontend SPA is served from the same mux as a catch-all handler.Core Interfaces
Every major subsystem is wired together through the interfaces defined ininternal/core/interfaces.go. Depending on these interfaces rather than concrete types lets you swap out the LLM provider, messaging platform, or tool registry without touching the Engine.
DefaultDispatcher in internal/core/dispatcher.go is the standard MessageDispatcher implementation. It holds a map[string]MessageAdapter guarded by a sync.RWMutex, so adapters for multiple platforms can be registered concurrently.
Message Flow (Step by Step)
- Upgrade —
HandleWSupgrades the raw HTTP request to a WebSocket connection and wraps it in awsConnectionto serialise writes with async.Mutex. - Read loop — The goroutine reads frames in a blocking loop; heartbeat frames are silently dropped.
- Dispatch goroutine — Each non-heartbeat event spawns
go processEvent(wsConn, event, engine). - Parse —
processEventunmarshals the OneBot message segments and checks whether the bot was @-mentioned (group chats only). - Session lookup —
engine.SessionManager.GetOrCreate(sessionID)returns or creates aSessionContext;session.Snapshot()deep-copies the history. - Prompt assembly — User text and a JSON system context block are combined into a single prompt string.
- History append — The user message is appended to the session via
session.AddMessage. - RunMessages —
engine.RunMessages(messages)starts the agentic loop. - LLM call —
Provider.Chatis called; the response is checked for tool calls. - Tool execution — Each requested tool is located in
ToolRegistryand executed; the JSON result is appended as arole: "tool"message. - SendHook — If the tool is
send_message,SendHookfires immediately, sending the message to the user before returning control to the LLM. - Final answer — When the LLM responds with no tool calls,
RunMessagesreturns the content string. - History trim — The updated message slice is trimmed to
MaxHistorymessages (preserving the system prompt and tool-call chains) and written back to the session. - Reply — The adapter serialises the final answer into an OneBot action and writes it to the WebSocket.
Concurrency Model
FrostAgent is designed for concurrent message handling from the ground up:- Per-connection goroutine — Each WebSocket connection runs its own read loop. The
gorilla/websocketlibrary is not concurrency-safe for writes, so all writes go throughwsConnection.writeMu(sync.Mutex). - Per-event goroutine —
processEventis always called withgo, so slow LLM calls on one event cannot block receipt of the next. - Session mutex —
SessionContext.mu(sync.Mutex) protectsHistoryandUpdatedAt.RunWithSessionholds the lock for the full duration of the agentic loop; external callers useLock()/Unlock()explicitly.Snapshot()andReplaceMessages()acquire the lock internally. - SessionManager RWMutex —
SessionManager.mu(sync.RWMutex) guards the session map, allowing concurrent reads while serialising writes (create / delete / cleanup). - DefaultDispatcher RWMutex — Similarly guards the adapter map with
sync.RWMutex.