Documentation
¶
Overview ¶
Package agent — compaction.go implements LLM-powered context compaction (REQ-400, REQ-401).
When context exceeds the pruning threshold and surgical pruning is insufficient, the compaction system:
- Splits messages into token-budgeted chunks (adaptive ratio: base 0.4, min 0.15)
- Strips tool result details before summarization (DEC-051)
- Summarizes each chunk via the active model with identifier preservation
- Merges summaries preserving active tasks, decisions, TODOs, commitments
- Falls back progressively: full → partial → note → hard clear
The context window guard (REQ-401) hard-blocks models with <16K context and warns about models with <32K context.
Package agent — loopdetect.go implements multi-detector tool loop detection (REQ-410, REQ-411).
Four detectors run on each tool call:
- Generic repeat — same tool+args N times (warn@10, critical@20)
- Known poll no-progress — poll-like calls with identical outcomes (warn@10, critical@20)
- Ping-pong — alternating between two patterns with no progress (warn@10, critical@20)
- Global circuit breaker — any single tool+args repeated 30 times = hard stop
Tool calls are hashed via name + SHA-256(stableJSON(params)). A sliding window of the last historySize calls is maintained per session.
Package agent — usage.go implements normalized token usage tracking (REQ-420).
After each model API call the raw provider response is normalized into a standard 5-field struct (Input, Output, CacheRead, CacheWrite, Total) and accumulated per-session in memory. The tracker exposes per-session and aggregate queries for the dashboard (REQ-302) and WS status feed.
Index ¶
- Constants
- func BuildCLISystemPrompt(def *config.AgentDef, skillList []skills.Skill, wsMDs map[string]string, ...) string
- func CompactHistory(ctx context.Context, logger *zap.SugaredLogger, client CompactorClient, ...) ([]session.Message, error)
- func FormatCapabilities(caps []config.Capability) string
- func ResolveContextWindow(perModelConfig, agentDefault int) int
- func ValidateContextWindow(logger *zap.SugaredLogger, contextTokens int) error
- type Agent
- func (a *Agent) Chat(ctx context.Context, sessionKey, userText string) (Response, error)
- func (a *Agent) ChatLight(ctx context.Context, sessionKey, userText string) (Response, error)
- func (a *Agent) ChatStream(ctx context.Context, sessionKey, userText string, cb *StreamCallbacks) (Response, error)
- func (a *Agent) ChatWithImages(ctx context.Context, sessionKey, userText string, imageURLs []string) (Response, error)
- func (a *Agent) ClearSessionModel(key string)
- func (a *Agent) Compact(ctx context.Context, sessionKey, instructions string) error
- func (a *Agent) GetConfig() *config.Root
- func (a *Agent) GetUsage() *UsageTracker
- func (a *Agent) ModelHealth() []models.ModelHealthStatus
- func (a *Agent) ResolveModel(key string) string
- func (a *Agent) SetEidetic(client eidetic.Client)
- func (a *Agent) SetEmbeddings(client *embeddings.Client)
- func (a *Agent) SetSessionModel(key, model string)
- func (a *Agent) UpdateConfig(newCfg *config.Root)
- type Announcer
- type CLIAgent
- type Chatter
- type CompactorClient
- type ContextWindowError
- type DelegateTool
- type DispatchTool
- type LoopDetectionLevel
- type LoopDetectionResult
- type LoopDetectorKind
- type MatchResult
- type ModelClient
- type NormalizedUsage
- type PollBackoff
- type PrimaryAgent
- type Response
- type ResponseUsage
- type SessionUsage
- type StreamCallbacks
- type StreamingCLIAgent
- func (s *StreamingCLIAgent) Chat(ctx context.Context, sessionKey, message string) (Response, error)
- func (s *StreamingCLIAgent) ChatLight(ctx context.Context, sessionKey, message string) (Response, error)
- func (s *StreamingCLIAgent) ChatStream(ctx context.Context, sessionKey, message string, cb *StreamCallbacks) (Response, error)
- func (s *StreamingCLIAgent) ChatWithImages(ctx context.Context, sessionKey, caption string, imageURLs []string) (Response, error)
- func (s *StreamingCLIAgent) ClearSessionModel(key string)
- func (s *StreamingCLIAgent) Close()
- func (s *StreamingCLIAgent) Compact(ctx context.Context, sessionKey, instructions string) error
- func (s *StreamingCLIAgent) GetUsage() *UsageTracker
- func (s *StreamingCLIAgent) ModelHealth() []models.ModelHealthStatus
- func (s *StreamingCLIAgent) ResolveModel(key string) string
- func (s *StreamingCLIAgent) SetEidetic(client eidetic.Client)
- func (s *StreamingCLIAgent) SetEmbeddings(client *embeddings.Client)
- func (s *StreamingCLIAgent) SetSessionModel(key, model string)
- type StreamingCLIConfig
- type Tool
- type ToolCallRecord
- type ToolLoopDetectionConfig
- type ToolLoopDetector
- type UsageTracker
- func (t *UsageTracker) Accumulate(sessionKey string, usage NormalizedUsage)
- func (t *UsageTracker) Aggregate() NormalizedUsage
- func (t *UsageTracker) ClearSession(sessionKey string)
- func (t *UsageTracker) GetAll() map[string]SessionUsage
- func (t *UsageTracker) GetSession(sessionKey string) (NormalizedUsage, int)
Constants ¶
const ( SummarizationOverheadTokens = 4096 BaseChunkRatio = 0.4 MinChunkRatio = 0.15 SafetyMargin = 1.2 // Context window guard thresholds (REQ-401) ContextWindowMinimum = 16_000 // hard block below this ContextWindowWarning = 32_000 // warning below this )
Compaction constants per architecture spec.
Variables ¶
This section is empty.
Functions ¶
func BuildCLISystemPrompt ¶
func BuildCLISystemPrompt(def *config.AgentDef, skillList []skills.Skill, wsMDs map[string]string, configPrompt string) string
BuildCLISystemPrompt constructs a system prompt for the claude-cli engine, combining identity, core rules, skills, workspace docs, and an optional config-level system prompt override. This mirrors initStaticPrompt but is a standalone function usable without an Agent instance.
func CompactHistory ¶
func CompactHistory(ctx context.Context, logger *zap.SugaredLogger, client CompactorClient, model string, msgs []session.Message, maxTokens, keepN int) ([]session.Message, error)
CompactHistory runs LLM-powered compaction on messages that exceed the token budget. It returns a compacted message list or the original messages if compaction fails. keepN is the number of recent assistant turns to preserve intact.
func FormatCapabilities ¶
func FormatCapabilities(caps []config.Capability) string
FormatCapabilities returns a concise string describing an agent's capabilities for injection into system prompts. Format: "code-generation [golang, python] (0.9), code-review [golang] (0.8)" Strength is omitted when 1.0 (default).
func ResolveContextWindow ¶
ResolveContextWindow determines the effective context window size. Priority: perModelConfig > modelDefault > agentDefault > globalFallback (128K).
func ValidateContextWindow ¶
func ValidateContextWindow(logger *zap.SugaredLogger, contextTokens int) error
ValidateContextWindow checks if the resolved context window meets minimum requirements. Returns an error if below ContextWindowMinimum; logs a warning if below ContextWindowWarning.
Types ¶
type Agent ¶
type Agent struct {
// Lifecycle hook bus (nil = no-op)
Hooks *hooks.Bus
// Usage tracks normalized token usage per session (REQ-420)
Usage *UsageTracker
// contains filtered or unexported fields
}
Agent handles the conversation loop for one agent definition.
func New ¶
func New( logger *zap.SugaredLogger, cfg *config.Root, def *config.AgentDef, router *models.Router, sessions *session.Manager, skillList []skills.Skill, wsMDs map[string]string, workspace string, toolList []Tool, ) *Agent
New creates an Agent.
func (*Agent) Chat ¶
Chat processes a single user message and returns the assistant response. sessionKey identifies the conversation (e.g. "agent:main:telegram:12345").
func (*Agent) ChatLight ¶
ChatLight is like Chat but uses a minimal system prompt (identity + HEARTBEAT.md only). Used for cron/heartbeat jobs with lightContext enabled.
func (*Agent) ChatStream ¶
func (a *Agent) ChatStream(ctx context.Context, sessionKey, userText string, cb *StreamCallbacks) (Response, error)
ChatStream is like Chat but provides real-time visibility via callbacks.
func (*Agent) ChatWithImages ¶
func (a *Agent) ChatWithImages(ctx context.Context, sessionKey, userText string, imageURLs []string) (Response, error)
ChatWithImages processes a user message with attached images (base64 data URLs). The images are included as multi-content parts in the user message for vision models.
func (*Agent) ClearSessionModel ¶
ClearSessionModel removes a per-session model override.
func (*Agent) GetConfig ¶
GetConfig returns the current config. Exported for use by heartbeat runner etc.
func (*Agent) GetUsage ¶
func (a *Agent) GetUsage() *UsageTracker
GetUsage returns the agent's usage tracker.
func (*Agent) ModelHealth ¶
func (a *Agent) ModelHealth() []models.ModelHealthStatus
ModelHealth returns the health status of configured models (registration + cooldown).
func (*Agent) ResolveModel ¶
ResolveModel returns the model for a session (override if set, else config default).
func (*Agent) SetEidetic ¶
SetEidetic wires an Eidetic client into the agent. Pass nil to disable. Safe to call after New() and concurrently with requests.
func (*Agent) SetEmbeddings ¶
func (a *Agent) SetEmbeddings(client *embeddings.Client)
SetEmbeddings wires an embeddings client into the agent. Pass nil to disable. Safe to call after New() and concurrently with requests.
func (*Agent) SetSessionModel ¶
SetSessionModel sets a per-session model override.
func (*Agent) UpdateConfig ¶
UpdateConfig swaps the agent's config and updates the router's model list. Called by the hot-reload callback when the config file changes.
type CLIAgent ¶
type CLIAgent struct {
// contains filtered or unexported fields
}
CLIAgent implements Chatter by invoking an external CLI command. Each Chat call spawns a fresh subprocess: command [args...] message. This is the mechanism used for CLI-backed subagents such as Claude Code.
func NewCLIAgent ¶
NewCLIAgent creates a CLIAgent. timeout=0 means no additional timeout (the caller's context deadline still applies). The command is resolved via exec.LookPath at construction time so that bare names (e.g. "claude") work even when the subprocess environment has a minimal PATH (e.g. under systemd).
type CompactorClient ¶
type CompactorClient interface {
Chat(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
}
CompactorClient is the model interface needed for compaction.
type ContextWindowError ¶
ContextWindowError is returned when the context window is too small.
func (*ContextWindowError) Error ¶
func (e *ContextWindowError) Error() string
type DelegateTool ¶
type DelegateTool struct {
// Agents maps agent ID → Chatter. The main agent itself may be included.
Agents map[string]Chatter
// AgentDefs maps agent ID → AgentDef for capability information in prompts.
AgentDefs map[string]*config.AgentDef
// AsyncAgents is the set of agent IDs that should run asynchronously via TaskMgr.
AsyncAgents map[string]bool
// MainAgentID is the ID of the main agent in the Agents map.
// When set, async results are routed through the main agent for
// summarization before being announced to the channel.
MainAgentID string
// DefaultModel is applied to subagent calls when the caller does not
// specify an explicit model override. Sourced from config
// agents.defaults.subagents.model. Empty = inherit the subagent's own default.
DefaultModel string
// MaxDepth is the maximum recursion depth (default 5).
MaxDepth int
// Announcers deliver async results back to the originating session.
Announcers []Announcer
// TaskMgr is the task queue manager for async tasks.
TaskMgr *taskqueue.Manager
// Logger is the injected structured logger.
Logger *zap.SugaredLogger
// AnnounceMaxRetries overrides the default retry count for async result
// announcement. Zero (default) uses defaultAnnounceMaxRetries.
AnnounceMaxRetries int
// AnnounceBaseBackoffMs overrides the default backoff for async result
// announcement retries. Zero (default) uses defaultAnnounceBaseBackoffMs.
AnnounceBaseBackoffMs int
}
DelegateTool is a Tool that lets the main agent call a named subagent. It lives in the agent package to avoid a circular import with internal/tools.
func (*DelegateTool) Description ¶
func (t *DelegateTool) Description() string
func (*DelegateTool) Name ¶
func (t *DelegateTool) Name() string
func (*DelegateTool) Schema ¶
func (t *DelegateTool) Schema() json.RawMessage
func (*DelegateTool) SetAgentDefs ¶
func (t *DelegateTool) SetAgentDefs(defs map[string]*config.AgentDef)
SetAgentDefs sets the AgentDefs map on DelegateTool for capability-based prompt injection.
type DispatchTool ¶
type DispatchTool struct {
// Agents maps agent ID → Chatter (same registry as DelegateTool).
Agents map[string]Chatter
// AgentDefs maps agent ID → AgentDef for capability-based routing.
AgentDefs map[string]*config.AgentDef
// MaxConcurrent limits simultaneous subtasks (default 5).
MaxConcurrent int
// ProgressFn is called after each task completes (optional).
ProgressFn orchestrator.ProgressFunc
// Announcers deliver dispatch acknowledgements and progress to the user's channel (REQ-160/161).
Announcers []Announcer
// ProgressUpdates enables per-task completion announcements (REQ-161).
ProgressUpdates bool
// TaskMgr submits the dispatch as a managed background task when set.
TaskMgr *taskqueue.Manager
// MainAgentID is the ID of the main agent in the Agents map.
// When set, async results are routed through the main agent for
// summarization before being announced to the channel.
MainAgentID string
// Logger is the injected structured logger.
Logger *zap.SugaredLogger
}
DispatchTool lets an orchestrator agent execute a task graph against registered subagents. The orchestrator LLM produces a JSON task graph, and this tool runs it through orchestrator.Dispatcher.
func (*DispatchTool) Description ¶
func (t *DispatchTool) Description() string
func (*DispatchTool) Name ¶
func (t *DispatchTool) Name() string
func (*DispatchTool) Schema ¶
func (t *DispatchTool) Schema() json.RawMessage
func (*DispatchTool) SetAgentDefs ¶
func (t *DispatchTool) SetAgentDefs(defs map[string]*config.AgentDef)
SetAgentDefs sets the AgentDefs map on DispatchTool for capability-based routing.
type LoopDetectionLevel ¶
type LoopDetectionLevel string
LoopDetectionLevel indicates the severity of a loop detection finding.
const ( LoopLevelNone LoopDetectionLevel = "" LoopLevelWarning LoopDetectionLevel = "warning" LoopLevelCritical LoopDetectionLevel = "critical" )
type LoopDetectionResult ¶
type LoopDetectionResult struct {
Stuck bool
Level LoopDetectionLevel
Detector LoopDetectorKind
Message string
}
LoopDetectionResult is returned by DetectLoop with the detection outcome.
type LoopDetectorKind ¶
type LoopDetectorKind string
LoopDetectorKind identifies which detector triggered.
const ( DetectorGenericRepeat LoopDetectorKind = "generic_repeat" DetectorKnownPollNoProgress LoopDetectorKind = "known_poll_no_progress" DetectorPingPong LoopDetectorKind = "ping_pong" DetectorGlobalCircuitBreaker LoopDetectorKind = "global_circuit_breaker" )
type MatchResult ¶
type MatchResult struct {
AgentID string `json:"agentId"`
Score float64 `json:"score"`
MatchedCap string `json:"matchedCap"`
}
MatchResult represents a scored agent match from capability-based routing.
func MatchCapabilities ¶
func MatchCapabilities(agents map[string]*config.AgentDef, taskDescription string) []MatchResult
MatchCapabilities scores agents against a task description using keyword matching. Returns results sorted by score descending. Only returns agents with score > 0.
type ModelClient ¶
type ModelClient interface {
Chat(ctx context.Context, req openai.ChatCompletionRequest) (openai.ChatCompletionResponse, error)
}
ModelClient is the interface for making model API calls (used for soft trim summarization).
type NormalizedUsage ¶
type NormalizedUsage struct {
Input int `json:"input"`
Output int `json:"output"`
CacheRead int `json:"cacheRead"`
CacheWrite int `json:"cacheWrite"`
Total int `json:"total"`
}
NormalizedUsage is a provider-agnostic token usage record.
func NormalizeUsage ¶
func NormalizeUsage(raw map[string]any) NormalizedUsage
NormalizeUsage maps provider-specific field names to the standard 5-field format. It handles 15+ naming variants from Anthropic, OpenAI, Google, Copilot, etc.
type PollBackoff ¶
type PollBackoff struct {
// contains filtered or unexported fields
}
PollBackoff tracks per-command poll counts and suggests backoff delays.
func NewPollBackoff ¶
func NewPollBackoff() *PollBackoff
NewPollBackoff creates a new backoff tracker.
type PrimaryAgent ¶
type PrimaryAgent interface {
Chat(ctx context.Context, sessionKey, message string) (Response, error)
ChatStream(ctx context.Context, sessionKey, message string, cb *StreamCallbacks) (Response, error)
ChatWithImages(ctx context.Context, sessionKey, caption string, imageURLs []string) (Response, error)
ChatLight(ctx context.Context, sessionKey, message string) (Response, error)
Compact(ctx context.Context, sessionKey, instructions string) error
SetSessionModel(key, model string)
ClearSessionModel(key string)
ResolveModel(key string) string
ModelHealth() []models.ModelHealthStatus
GetUsage() *UsageTracker
}
PrimaryAgent is the interface satisfied by both *Agent (router-backed) and *StreamingCLIAgent (claude-cli-backed). All channel bots, the gateway, and the command handler accept this interface instead of a concrete *Agent.
type ResponseUsage ¶
type ResponseUsage = agentapi.ResponseUsage
ResponseUsage is an alias for agentapi.ResponseUsage.
type SessionUsage ¶
type SessionUsage struct {
Cumulative NormalizedUsage `json:"cumulative"`
Calls int `json:"calls"`
// contains filtered or unexported fields
}
SessionUsage tracks cumulative token usage for a single session.
type StreamCallbacks ¶
type StreamCallbacks struct {
OnChunk func(text string) // streamed text delta
OnThinking func(text string) // extended thinking delta
OnToolStart func(name string, args string) // tool about to execute
OnToolDone func(name string, result string, err error) // tool finished
OnIterationText func(text string) // intermediate text block emitted alongside tool calls (between iterations)
}
StreamCallbacks groups optional callbacks for real-time visibility into the agent loop. All fields are optional; nil callbacks are silently skipped.
type StreamingCLIAgent ¶
type StreamingCLIAgent struct {
// contains filtered or unexported fields
}
StreamingCLIAgent implements PrimaryAgent by driving a long-lived `claude -p --input-format stream-json --output-format stream-json` subprocess. Each session gets its own subprocess; idle subprocesses are reaped after a TTL.
func NewStreamingCLIAgent ¶
func NewStreamingCLIAgent(logger *zap.SugaredLogger, cfg StreamingCLIConfig) (*StreamingCLIAgent, error)
NewStreamingCLIAgent creates a StreamingCLIAgent.
func (*StreamingCLIAgent) ChatStream ¶
func (s *StreamingCLIAgent) ChatStream(ctx context.Context, sessionKey, message string, cb *StreamCallbacks) (Response, error)
func (*StreamingCLIAgent) ChatWithImages ¶
func (*StreamingCLIAgent) ClearSessionModel ¶
func (s *StreamingCLIAgent) ClearSessionModel(key string)
func (*StreamingCLIAgent) Compact ¶
func (s *StreamingCLIAgent) Compact(ctx context.Context, sessionKey, instructions string) error
func (*StreamingCLIAgent) GetUsage ¶
func (s *StreamingCLIAgent) GetUsage() *UsageTracker
func (*StreamingCLIAgent) ModelHealth ¶
func (s *StreamingCLIAgent) ModelHealth() []models.ModelHealthStatus
func (*StreamingCLIAgent) ResolveModel ¶
func (s *StreamingCLIAgent) ResolveModel(key string) string
func (*StreamingCLIAgent) SetEidetic ¶
func (s *StreamingCLIAgent) SetEidetic(client eidetic.Client)
SetEidetic wires an Eidetic client into the agent. Pass nil to disable.
func (*StreamingCLIAgent) SetEmbeddings ¶
func (s *StreamingCLIAgent) SetEmbeddings(client *embeddings.Client)
SetEmbeddings wires an embeddings client for hybrid search. Pass nil to disable.
func (*StreamingCLIAgent) SetSessionModel ¶
func (s *StreamingCLIAgent) SetSessionModel(key, model string)
type StreamingCLIConfig ¶
type StreamingCLIConfig struct {
Command string // path or name of the claude binary (default "claude")
ExtraArgs []string // additional CLI flags (e.g. --mcp-config, --system-prompt)
Model string // model to request (e.g. "sonnet")
IdleTTL time.Duration
MCPConfig string // path to MCP config JSON for Roger tools
SystemPrompt string // static base system prompt (identity, skills, workspace docs)
// Memory integration
Config *config.Root // full config for eidetic settings, timezone, etc.
Workspace string // filesystem path for MEMORY.md, daily logs
}
StreamingCLIConfig configures a StreamingCLIAgent.
type ToolCallRecord ¶
type ToolCallRecord struct {
ToolName string
ArgsHash string // SHA-256 of stableJSON(params)
ResultHash string // SHA-256 of outcome (set after execution)
CallHash string // name:argsHash composite key
TS time.Time
}
ToolCallRecord tracks a single tool call in the sliding window.
type ToolLoopDetectionConfig ¶
type ToolLoopDetectionConfig struct {
Enabled bool `json:"enabled"`
HistorySize int `json:"historySize"`
WarningThreshold int `json:"warningThreshold"`
CriticalThreshold int `json:"criticalThreshold"`
GlobalCircuitBreakerThreshold int `json:"globalCircuitBreakerThreshold"`
GenericRepeat bool `json:"genericRepeat"`
KnownPollNoProgress bool `json:"knownPollNoProgress"`
PingPong bool `json:"pingPong"`
}
ToolLoopDetectionConfig holds thresholds for the multi-detector system.
func DefaultToolLoopDetectionConfig ¶
func DefaultToolLoopDetectionConfig() ToolLoopDetectionConfig
DefaultToolLoopDetectionConfig returns a config with sensible defaults.
type ToolLoopDetector ¶
type ToolLoopDetector struct {
// contains filtered or unexported fields
}
ToolLoopDetector tracks tool call history and runs multi-detector analysis.
func NewToolLoopDetector ¶
func NewToolLoopDetector(cfg ToolLoopDetectionConfig) *ToolLoopDetector
NewToolLoopDetector creates a detector with the given config.
func (*ToolLoopDetector) DetectLoop ¶
func (d *ToolLoopDetector) DetectLoop() LoopDetectionResult
DetectLoop runs all enabled detectors and returns the highest-severity finding.
func (*ToolLoopDetector) RecordCall ¶
func (d *ToolLoopDetector) RecordCall(toolName, argsJSON string) int
RecordCall adds a tool call to the sliding window. Returns the record index.
func (*ToolLoopDetector) RecordOutcome ¶
func (d *ToolLoopDetector) RecordOutcome(idx int, result string)
RecordOutcome sets the result hash for the most recent call at the given index.
type UsageTracker ¶
type UsageTracker struct {
// contains filtered or unexported fields
}
UsageTracker accumulates normalized token usage per session (in-memory, no persistence).
func NewUsageTracker ¶
func NewUsageTracker() *UsageTracker
NewUsageTracker creates a new usage tracker.
func (*UsageTracker) Accumulate ¶
func (t *UsageTracker) Accumulate(sessionKey string, usage NormalizedUsage)
Accumulate adds usage from a single model call to the session's cumulative total.
func (*UsageTracker) Aggregate ¶
func (t *UsageTracker) Aggregate() NormalizedUsage
Aggregate returns the total usage across all sessions.
func (*UsageTracker) ClearSession ¶
func (t *UsageTracker) ClearSession(sessionKey string)
ClearSession removes usage data for a session (e.g. on session reset).
func (*UsageTracker) GetAll ¶
func (t *UsageTracker) GetAll() map[string]SessionUsage
GetAll returns a snapshot of all session usage.
func (*UsageTracker) GetSession ¶
func (t *UsageTracker) GetSession(sessionKey string) (NormalizedUsage, int)
GetSession returns cumulative usage for a session. Returns zero if not found.