Documentation
¶
Overview ¶
Package agent implements the chatbot's agentic tool-use loop. It orchestrates the conversation between the LLM and MCP tool servers, streaming events back to the caller.
Design ref: docs/chatbot-design-spec.md §2.2
Index ¶
- Constants
- Variables
- func BuildSystemPrompt(cfg SystemPromptConfig, router mcpclient.ToolRouter, interestIndex string, ...) string
- func BuildSystemPromptWithRouting(cfg SystemPromptConfig, router mcpclient.ToolRouter, ...) string
- func ValidToolRoutingMode(mode string) bool
- type Agent
- type CanvasPayload
- type CanvasTabSummary
- type Event
- type EventType
- type InternalTool
- type InternalToolHandler
- type Option
- func WithApprovalFunc(fn func(capID, toolName string, tc llm.ToolCall) bool) Option
- func WithCapabilityFilter(states capability.CapabilityMap, mode string) Option
- func WithInternalTools(tools map[string]InternalTool) Option
- func WithLogger(l *slog.Logger) Option
- func WithMaxIterations(n int) Option
- func WithModel(model string) Option
- func WithSystemPrompt(prompt string) Option
- func WithToolRouting(mode string, groups []capability.Group, alwaysOn []string, maxTools int, ...) Option
- func WithToolServerMap(m map[string]string) Option
- type RoutingStats
- type SystemPromptConfig
Constants ¶
const ( // ToolRoutingOff disables the supervisor. Tool selection is byte-for-byte // identical to the pre-S7 behavior: every enabled capability's tools are // sent on every turn. This is the default. ToolRoutingOff = "off" // ToolRoutingInBand enables the in-band supervisor (S7a): the main model // self-selects capability groups via the load_tools internal tool before // the worker loop loads those groups' tools. No dedicated routing call. ToolRoutingInBand = "in-band" // ToolRoutingRouter selects the dedicated router model (S7b). Parsed and // validated as a legal mode but not yet implemented; startup wiring // rejects it until the S7b path is built. ToolRoutingRouter = "router" )
Tool-routing modes (S7 supervisor). These are the canonical values for the tool_routing.mode config key; config validation and the agent loop both reference them so there is a single source of truth.
const DefaultMaxIterations = 10
DefaultMaxIterations is the safety limit for tool-call rounds per user message. After this many iterations the LLM is asked to summarize.
const MaxToolsPerRequest = 128
MaxToolsPerRequest is the hard cap on tools sent to the LLM per request. Azure OpenAI rejects requests with > 128 tools (HTTP 400), and OpenAI accuracy degrades sharply above ~20 tools per turn. Enforced in (*Agent).filteredTools.
Variables ¶
var ErrTooManyTools = errors.New("too many tools enabled")
ErrTooManyTools is returned by filteredTools when the assembled tool list exceeds MaxToolsPerRequest. The chat handler should surface a clear message to the user instead of letting the LLM call fail.
Functions ¶
func BuildSystemPrompt ¶
func BuildSystemPrompt(cfg SystemPromptConfig, router mcpclient.ToolRouter, interestIndex string, canvasTabs ...CanvasTabSummary) string
BuildSystemPrompt constructs the system prompt from the current state. This is a convenience to generate the prompt that includes tool context. If interestIndex is non-empty, the chart format spec and interest catalog are appended so the LLM knows how to produce dashboard panels. If canvasTabs is non-empty, a canvas context section is appended so the LLM knows what items the user has pinned in the canvas.
This is the no-tool-routing form, byte-for-byte identical to today. For the in-band supervisor (S7a), use BuildSystemPromptWithRouting.
func BuildSystemPromptWithRouting ¶ added in v0.1.16
func BuildSystemPromptWithRouting(cfg SystemPromptConfig, router mcpclient.ToolRouter, interestIndex, groupIndex string, canvasTabs ...CanvasTabSummary) string
BuildSystemPromptWithRouting is BuildSystemPrompt plus an optional in-band tool-routing group index (S7a). When groupIndex is empty the output is byte-for-byte identical to BuildSystemPrompt — this is the mode:off guarantee. When groupIndex is non-empty, a "Tool Groups" section is appended instructing the model to call load_tools(group) before answering, reusing the forced-first-step contract proven by the interest path.
func ValidToolRoutingMode ¶ added in v0.1.16
ValidToolRoutingMode reports whether mode is a recognized tool_routing mode. An empty string is treated as ToolRoutingOff and is considered valid.
Types ¶
type Agent ¶
type Agent struct {
Provider llm.Provider
Router mcpclient.ToolRouter
SystemPrompt string
Model string
MaxIterations int
Logger *slog.Logger
// Phase 2: capability filtering
CapStates capability.CapabilityMap // nil = no filtering
Mode string // "read-only" or "read-write"
// ToolServerMap maps tool name -> capability ID for ask-mode routing.
// Populated at agent creation from the router.
ToolServerMap map[string]string
// ApprovalFunc is called when a tool requires user approval (Ask mode).
// It returns true if approved. If nil, ask-mode tools are auto-approved.
ApprovalFunc func(capID, toolName string, tc llm.ToolCall) bool
// InternalTools are handled locally by the agent, not routed through MCP.
// Keyed by tool name.
InternalTools map[string]InternalTool
// ToolRoutingMode is one of ToolRoutingOff (default), ToolRoutingInBand,
// or ToolRoutingRouter. Set via WithToolRouting.
ToolRoutingMode string
// AlwaysOnGroups are capability/group IDs loaded from the first turn
// without the model calling load_tools.
AlwaysOnGroups []string
// MaxRoutedTools optionally caps the post-routing tool list (0 = no cap
// beyond MaxToolsPerRequest).
MaxRoutedTools int
// ForceGroupLoad, when true, makes the agent inject one corrective nudge
// if the model tries to answer (tool-lessly) before loading any group
// while groups are available. Defaults on for in-band via WithToolRouting.
ForceGroupLoad bool
// contains filtered or unexported fields
}
Agent runs the tool-use loop. It holds the LLM provider and MCP router.
func (*Agent) LastRoutingStats ¶ added in v0.1.16
func (a *Agent) LastRoutingStats() RoutingStats
LastRoutingStats returns a copy of the telemetry from the most recent Run. Meaningful only when in-band routing is enabled.
func (*Agent) Run ¶
Run executes the agentic tool-use loop for a user message. It calls the provided emit function for each event. The conversation history is carried in messages; the caller manages session state.
The loop:
- Gathers tools from the Router
- Sends messages + tools to the LLM via streaming
- On text tokens -> emit EventText
- On tool calls -> execute via Router, emit EventToolStart/Result/Error, append results to messages and re-send to LLM
- Repeat until the LLM produces a text-only response or max iterations
- Emit EventDone
type CanvasPayload ¶
type CanvasPayload struct {
TabID string `json:"tab_id"`
Title string `json:"title"`
Kind string `json:"kind"`
Qualifier string `json:"qualifier"`
Content json.RawMessage `json:"content"`
}
CanvasPayload holds the data for a canvas_open SSE event.
type CanvasTabSummary ¶
type CanvasTabSummary struct {
TabID string `json:"tab_id"`
Kind string `json:"kind"`
Name string `json:"name"`
Qualifier string `json:"qualifier"`
Status string `json:"status,omitempty"`
KeyProperties map[string]string `json:"key_properties,omitempty"`
}
CanvasTabSummary is a compact description of an open canvas tab, sent from the frontend to give the LLM context about what the user can currently see pinned in the canvas.
type Event ¶
type Event struct {
Type EventType `json:"type"`
Text string `json:"text,omitempty"` // for EventText
ToolCall *llm.ToolCall `json:"tool_call,omitempty"` // for EventToolStart
ToolName string `json:"tool_name,omitempty"` // for EventToolResult / EventToolError
ToolResult string `json:"tool_result,omitempty"` // for EventToolResult
Error string `json:"error,omitempty"` // for EventToolError, EventError
Capability string `json:"capability,omitempty"` // MCP capability ID (Phase 2)
ApprovalID string `json:"approval_id,omitempty"` // for EventToolApprovalRequired (Phase 2)
Canvas *CanvasPayload `json:"canvas,omitempty"` // for EventCanvasOpen
}
Event is emitted by the agent loop to inform the caller about progress. The caller (typically the SSE handler) converts these to SSE events.
type EventType ¶
type EventType int
EventType enumerates agent-level event kinds.
const ( // EventText is a streamed text token from the LLM. EventText EventType = iota // EventToolStart signals that a tool call is about to execute. EventToolStart // EventToolResult carries the tool execution result. EventToolResult // EventToolError signals a tool execution failure (non-fatal, fed back to LLM). EventToolError // EventDone signals the end of the agent loop. EventDone // EventError signals a fatal error (loop stops). EventError // EventToolApprovalRequired signals that a tool call needs user approval (Ask mode). EventToolApprovalRequired // EventTextClear tells the UI to clear any accumulated assistant text. // Emitted when a streaming turn produced "thinking" text alongside tool // calls (common with Claude models). The text was shown during streaming // for feedback but should not persist into the final message. EventTextClear // EventCanvasOpen tells the UI to open content in a canvas tab. // Emitted when the LLM uses a canvas-object-detail or canvas-dashboard // code fence, signaling the content should be pinned rather than inline. EventCanvasOpen )
type InternalTool ¶
type InternalTool struct {
Def llm.ToolDef
Handler InternalToolHandler
ReadWriteOnly bool // if true, excluded when Mode is not "read-write"
EmitResult bool // if true, tool result is also emitted as EventText
// RequiredAfterInterest makes this tool mandatory when the named interest
// was loaded via get_interest. If the LLM finishes without calling the
// tool, the agent injects a system message and forces a retry. This
// prevents LLMs from skipping render tools.
RequiredAfterInterest string
}
InternalTool bundles a tool definition with its handler so the agent can advertise the tool to the LLM and execute it locally.
type InternalToolHandler ¶
InternalToolHandler is a function that handles an internal tool call. It receives the raw JSON input and returns a result string or error.
type Option ¶
type Option func(*Agent)
Option configures an Agent.
func WithApprovalFunc ¶
WithApprovalFunc sets the callback for ask-mode tool approval.
func WithCapabilityFilter ¶
func WithCapabilityFilter(states capability.CapabilityMap, mode string) Option
WithCapabilityFilter sets capability states and mode for tool filtering. Tools from capabilities in StateOff are excluded. In read-only mode, tools annotated as write/destructive are also excluded.
func WithInternalTools ¶
func WithInternalTools(tools map[string]InternalTool) Option
WithInternalTools registers tools that the agent handles locally instead of routing through MCP.
func WithMaxIterations ¶
WithMaxIterations sets the tool-call iteration limit.
func WithSystemPrompt ¶
WithSystemPrompt sets the system prompt.
func WithToolRouting ¶ added in v0.1.16
func WithToolRouting(mode string, groups []capability.Group, alwaysOn []string, maxTools int, forceGroupLoad bool) Option
WithToolRouting configures the in-band supervisor (S7a). mode selects the routing strategy (ToolRoutingOff disables it). groups is the auto-derived group menu for this turn (from capability.BuildGroups). alwaysOn lists groups loaded from turn 1. maxTools optionally caps the post-routing list (0 = no extra cap). forceGroupLoad enables the one-shot corrective nudge when the model answers before loading a group.
When mode is not ToolRoutingInBand this is inert; the agent behaves exactly as it does today.
func WithToolServerMap ¶
WithToolServerMap sets the tool-to-capability mapping for ask-mode routing.
type RoutingStats ¶ added in v0.1.16
type RoutingStats struct {
Mode string // routing mode for the run
GroupsOffered int // number of groups in the menu this turn
GroupsLoaded []string // final set of loaded group IDs (sorted; includes groups owning any individually-loaded tool)
ToolsLoaded []string // individually-loaded tool names (S8 intra-group selection; sorted)
LoadCalls int // number of load_tools invocations
Reloads int // load_tools invocations after the first (mid-task re-loads)
Skipped bool // a final answer was produced without ever loading a group while groups were available
Compliant bool // at least one group was loaded before finishing
}
RoutingStats captures per-run in-band routing telemetry. It is the empirical basis for the S7a→S7b graduation decision (skip / misroute rates). Read it via (*Agent).LastRoutingStats after a Run.
type SystemPromptConfig ¶
type SystemPromptConfig struct {
// ProductName is the assistant's display name (e.g. "NAbox Assistant").
ProductName string
// ProductDescription is the paragraph after the name describing the
// product context (monitoring stack, data sources, etc.).
ProductDescription string
// Guidelines are appended after the role section. Include any product-
// specific guidelines such as URL rewriting rules.
Guidelines []string
// Vocabulary is a free-form markdown block appended after the generic
// Guidelines and before the connected-data-sources list. Products use
// this to inject domain-specific guidance (entity kinds, link patterns,
// CLI proposal formats, etc.) without modifying the agent package. The
// chat service ships no vocabulary by default — empty string means no
// block is appended.
Vocabulary string
}
SystemPromptConfig configures the identity and domain context injected into the system prompt. Products supply their own config so the agent package remains product-agnostic.