agent

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2026 License: MIT Imports: 33 Imported by: 0

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:

  1. Splits messages into token-budgeted chunks (adaptive ratio: base 0.4, min 0.15)
  2. Strips tool result details before summarization (DEC-051)
  3. Summarizes each chunk via the active model with identifier preservation
  4. Merges summaries preserving active tasks, decisions, TODOs, commitments
  5. 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:

  1. Generic repeat — same tool+args N times (warn@10, critical@20)
  2. Known poll no-progress — poll-like calls with identical outcomes (warn@10, critical@20)
  3. Ping-pong — alternating between two patterns with no progress (warn@10, critical@20)
  4. 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

View Source
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

func ResolveContextWindow(perModelConfig, agentDefault int) int

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

func (a *Agent) Chat(ctx context.Context, sessionKey, userText string) (Response, error)

Chat processes a single user message and returns the assistant response. sessionKey identifies the conversation (e.g. "agent:main:telegram:12345").

func (*Agent) ChatLight

func (a *Agent) ChatLight(ctx context.Context, sessionKey, userText string) (Response, error)

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

func (a *Agent) ClearSessionModel(key string)

ClearSessionModel removes a per-session model override.

func (*Agent) Compact

func (a *Agent) Compact(ctx context.Context, sessionKey, instructions string) error

Compact forces a soft trim of the session and persists it to disk.

func (*Agent) GetConfig

func (a *Agent) GetConfig() *config.Root

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

func (a *Agent) ResolveModel(key string) string

ResolveModel returns the model for a session (override if set, else config default).

func (*Agent) SetEidetic

func (a *Agent) SetEidetic(client eidetic.Client)

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

func (a *Agent) SetSessionModel(key, model string)

SetSessionModel sets a per-session model override.

func (*Agent) UpdateConfig

func (a *Agent) UpdateConfig(newCfg *config.Root)

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 Announcer

type Announcer = agentapi.Announcer

Announcer is an alias for agentapi.Announcer.

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

func NewCLIAgent(id, command string, args []string, timeout time.Duration) *CLIAgent

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

func (*CLIAgent) Chat

func (a *CLIAgent) Chat(ctx context.Context, _ string, message string) (Response, error)

Chat runs the CLI command with the message appended as the final argument and returns its stdout as the response text.

func (*CLIAgent) Command

func (a *CLIAgent) Command() string

Command returns the resolved command path.

type Chatter

type Chatter = agentapi.Chatter

Chatter is an alias for agentapi.Chatter.

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

type ContextWindowError struct {
	ModelTokens int
	Minimum     int
}

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) Run

func (t *DelegateTool) Run(ctx context.Context, argsJSON string) 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) Run

func (t *DispatchTool) Run(ctx context.Context, argsJSON string) 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.

func (*PollBackoff) Record

func (pb *PollBackoff) Record(commandKey string, hasNewOutput bool) time.Duration

Record records a poll for a command. Returns the suggested delay. If hasNewOutput is true, the counter resets.

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 Response

type Response = agentapi.Response

Response is an alias for agentapi.Response.

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) Chat

func (s *StreamingCLIAgent) Chat(ctx context.Context, sessionKey, message string) (Response, error)

func (*StreamingCLIAgent) ChatLight

func (s *StreamingCLIAgent) ChatLight(ctx context.Context, sessionKey, message string) (Response, error)

func (*StreamingCLIAgent) ChatStream

func (s *StreamingCLIAgent) ChatStream(ctx context.Context, sessionKey, message string, cb *StreamCallbacks) (Response, error)

func (*StreamingCLIAgent) ChatWithImages

func (s *StreamingCLIAgent) ChatWithImages(ctx context.Context, sessionKey, caption string, imageURLs []string) (Response, error)

func (*StreamingCLIAgent) ClearSessionModel

func (s *StreamingCLIAgent) ClearSessionModel(key string)

func (*StreamingCLIAgent) Close

func (s *StreamingCLIAgent) Close()

Close kills all subprocesses.

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 Tool

type Tool = agentapi.Tool

Tool is an alias for agentapi.Tool.

func DefaultTools

func DefaultTools(cfg *config.Root, workspace string, env map[string]string) []Tool

DefaultTools creates the standard tool set from config.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL