Skip to main content

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.

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.

High-Level Message Flow

When a user sends a message to the bot, the following sequence unfolds:
1

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.
2

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.
3

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.
4

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.
5

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.
6

Final reply is sent

After RunMessages returns, the adapter serialises the LLM’s final text answer into an OneBot action frame (send_group_msg or send_private_msg) and writes it to the connection through the thread-safe wsConnection.WriteMessage.

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 in internal/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.
// LLMProvider defines the interface for LLM API calls.
type LLMProvider interface {
    Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
}

// AgentService defines the interface for agent message handling.
type AgentService interface {
    Handle(ctx context.Context, input IncomingMessage) ([]OutgoingMessage, error)
}

// MessageAdapter defines the interface for platform-specific message sending.
type MessageAdapter interface {
    Send(ctx context.Context, msg OutgoingMessage) error
    ID() string
}

// MessageDispatcher routes core-layer outputs to the correct adapter.
type MessageDispatcher interface {
    RegisterAdapter(adapter MessageAdapter)
    Dispatch(ctx context.Context, platform string, msg OutgoingMessage) error
}

// ToolRegistry defines the interface for tool registration and lookup.
type ToolRegistry interface {
    Register(t Tool)
    GetTool(name string) (Tool, bool)
    GetExecutor(name string) (func(args string) (string, error), bool)
    ListTools() []Tool
    Execute(name, args string) (string, error)
}

// Session defines a single conversation session.
type Session interface {
    ID() string
    AddMessage(msg ChatMessage)
    Messages() []ChatMessage
    Clear()
}

// SessionStore manages all active sessions.
type SessionStore interface {
    Get(sessionID string) (Session, bool)
    Create(sessionID string) Session
    Delete(sessionID string)
}
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)

  1. UpgradeHandleWS upgrades the raw HTTP request to a WebSocket connection and wraps it in a wsConnection to serialise writes with a sync.Mutex.
  2. Read loop — The goroutine reads frames in a blocking loop; heartbeat frames are silently dropped.
  3. Dispatch goroutine — Each non-heartbeat event spawns go processEvent(wsConn, event, engine).
  4. ParseprocessEvent unmarshals the OneBot message segments and checks whether the bot was @-mentioned (group chats only).
  5. Session lookupengine.SessionManager.GetOrCreate(sessionID) returns or creates a SessionContext; session.Snapshot() deep-copies the history.
  6. Prompt assembly — User text and a JSON system context block are combined into a single prompt string.
  7. History append — The user message is appended to the session via session.AddMessage.
  8. RunMessagesengine.RunMessages(messages) starts the agentic loop.
  9. LLM callProvider.Chat is called; the response is checked for tool calls.
  10. Tool execution — Each requested tool is located in ToolRegistry and executed; the JSON result is appended as a role: "tool" message.
  11. SendHook — If the tool is send_message, SendHook fires immediately, sending the message to the user before returning control to the LLM.
  12. Final answer — When the LLM responds with no tool calls, RunMessages returns the content string.
  13. History trim — The updated message slice is trimmed to MaxHistory messages (preserving the system prompt and tool-call chains) and written back to the session.
  14. 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/websocket library is not concurrency-safe for writes, so all writes go through wsConnection.writeMu (sync.Mutex).
  • Per-event goroutineprocessEvent is always called with go, so slow LLM calls on one event cannot block receipt of the next.
  • Session mutexSessionContext.mu (sync.Mutex) protects History and UpdatedAt. RunWithSession holds the lock for the full duration of the agentic loop; external callers use Lock() / Unlock() explicitly. Snapshot() and ReplaceMessages() acquire the lock internally.
  • SessionManager RWMutexSessionManager.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.

Build docs developers (and LLMs) love