plugin

package module
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package plugin provides lifecycle hooks for extending the agent loop, tool execution, and LLM calls. Based on ADK-Go's plugin system with Eino's context-returning handler pattern.

Plugins implement optional hook interfaces. The Manager runs hooks in registration order. Each hook receives and returns context.Context for state propagation (Eino pattern). First non-nil error stops the chain (ADK-Go pattern).

Index

Constants

View Source
const NoCap = 1e18 // effectively unlimited; avoids importing "math" in callers

NoCap is the value returned by RemainingDailyBudget when no daily cap is set. Callers may compare remaining >= plugin.NoCap to detect the unlimited case.

Variables

This section is empty.

Functions

This section is empty.

Types

type AgentHooks

type AgentHooks interface {
	// BeforeAgentRun fires before the ReAct loop starts. Return error to abort.
	BeforeAgentRun(ctx context.Context, inv *Invocation) (context.Context, error)
	// AfterAgentRun fires after the ReAct loop completes successfully.
	AfterAgentRun(ctx context.Context, inv *Invocation, result *RunResult) context.Context
	// OnAgentError fires when the agent run fails.
	OnAgentError(ctx context.Context, inv *Invocation, err error) context.Context
}

AgentHooks intercepts the agent run lifecycle.

type BudgetConfig

type BudgetConfig struct {
	DailyCap   float64     // Max daily spend in dollars (0 = unlimited)
	WeeklyCap  float64     // Max weekly spend in dollars (0 = unlimited)
	UsageStore *UsageStore // Optional DB-backed usage tracking (cross-process)
}

BudgetConfig configures spend limits.

type BudgetPlugin

type BudgetPlugin struct {
	// contains filtered or unexported fields
}

BudgetPlugin enforces daily/weekly LLM spend limits. Checks budget before each LLM call and aborts if exceeded. Based on Gollem's budget tracker pattern.

func NewBudgetPlugin

func NewBudgetPlugin(cfg BudgetConfig, logger *slog.Logger) *BudgetPlugin

NewBudgetPlugin creates a budget enforcement plugin.

func (*BudgetPlugin) AfterLLMCall

func (p *BudgetPlugin) AfterLLMCall(ctx context.Context, call *LLMCall, usage *TokenUsage) context.Context

func (*BudgetPlugin) BeforeLLMCall

func (p *BudgetPlugin) BeforeLLMCall(ctx context.Context, call *LLMCall) (context.Context, error)

func (*BudgetPlugin) CanAfford

func (p *BudgetPlugin) CanAfford() bool

CanAfford returns true if the budget has room for at least one more call. Best-effort check — the actual call may still be blocked by BeforeLLMCall.

func (*BudgetPlugin) Name

func (p *BudgetPlugin) Name() string

func (*BudgetPlugin) OnLLMError

func (p *BudgetPlugin) OnLLMError(ctx context.Context, call *LLMCall, err error) context.Context

func (*BudgetPlugin) RemainingDailyBudget

func (p *BudgetPlugin) RemainingDailyBudget() float64

RemainingDailyBudget returns the remaining daily budget in USD. Returns math.MaxFloat64 when no daily cap is configured (unlimited). The caller must import "math" to compare against math.MaxFloat64.

func (*BudgetPlugin) SeedFromUsageStore

func (p *BudgetPlugin) SeedFromUsageStore(ctx context.Context)

SeedFromUsageStore loads today's and this week's spend from the DB so the in-memory counters survive restarts. Must be called once after creation, before the plugin starts intercepting LLM calls. Without this, dailySpend resets to 0 on every restart, effectively resetting the cap each time the process restarts.

func (*BudgetPlugin) Stats

func (p *BudgetPlugin) Stats() BudgetStats

Stats returns current budget state for API/dashboard.

type BudgetStats

type BudgetStats struct {
	DailySpend   float64
	DailyCap     float64
	WeeklySpend  float64
	WeeklyCap    float64
	TotalCalls   int64 // Resets daily; tracks calls in current day
	Blocked      int64
	InputTokens  int64 // Resets daily; tracks input tokens in current day
	OutputTokens int64 // Resets daily; tracks output tokens in current day
}

BudgetStats holds current spend state.

type DailyTotals

type DailyTotals struct {
	TotalCalls   int
	InputTokens  int64
	OutputTokens int64
	CostUSD      float64
}

DailyTotals returns aggregated usage for today (UTC).

type Invocation

type Invocation struct {
	SessionID   string
	UserMessage string
	Mode        tool.Mode
	Model       string
	StartedAt   time.Time
}

Invocation carries per-run metadata.

type LLMCall

type LLMCall struct {
	Model      string
	Round      int
	ProviderID string // ID of the llm.Provider making this call (empty = unknown)
	CostType   string // billing model: "metered" (default), "flat_rate", or "free"
	// CacheReadRatePer1M is the USD cost per million cached input tokens for this
	// model (from the TOML catalog). Zero means no cache pricing is available.
	// Used by the budget plugin to compute accurate cost when cached tokens are present.
	CacheReadRatePer1M float64
	// StartedAt is set just before the LLM stream begins. AfterLLMCall uses it
	// to compute call duration for latency tracking.
	StartedAt time.Time
}

LLMCall carries LLM request context.

type LLMHooks

type LLMHooks interface {
	// BeforeLLMCall fires before an LLM request. Return error to abort.
	BeforeLLMCall(ctx context.Context, call *LLMCall) (context.Context, error)
	// AfterLLMCall fires after an LLM response is received.
	AfterLLMCall(ctx context.Context, call *LLMCall, usage *TokenUsage) context.Context
	// OnLLMError fires when an LLM call fails.
	OnLLMError(ctx context.Context, call *LLMCall, err error) context.Context
}

LLMHooks intercepts LLM calls.

type LoggingPlugin

type LoggingPlugin struct {
	// contains filtered or unexported fields
}

LoggingPlugin logs agent, tool, and LLM lifecycle events. Based on ADK-Go's loggingplugin.

func NewLoggingPlugin

func NewLoggingPlugin(logger *slog.Logger) *LoggingPlugin

NewLoggingPlugin creates a logging plugin.

func (*LoggingPlugin) AfterAgentRun

func (p *LoggingPlugin) AfterAgentRun(ctx context.Context, inv *Invocation, result *RunResult) context.Context

func (*LoggingPlugin) AfterLLMCall

func (p *LoggingPlugin) AfterLLMCall(ctx context.Context, call *LLMCall, usage *TokenUsage) context.Context

func (*LoggingPlugin) AfterToolCall

func (p *LoggingPlugin) AfterToolCall(ctx context.Context, call *ToolCall, result *ToolResult) context.Context

func (*LoggingPlugin) BeforeAgentRun

func (p *LoggingPlugin) BeforeAgentRun(ctx context.Context, inv *Invocation) (context.Context, error)

func (*LoggingPlugin) BeforeLLMCall

func (p *LoggingPlugin) BeforeLLMCall(ctx context.Context, call *LLMCall) (context.Context, error)

func (*LoggingPlugin) BeforeToolCall

func (p *LoggingPlugin) BeforeToolCall(ctx context.Context, call *ToolCall) (context.Context, error)

func (*LoggingPlugin) Name

func (p *LoggingPlugin) Name() string

func (*LoggingPlugin) OnAgentError

func (p *LoggingPlugin) OnAgentError(ctx context.Context, inv *Invocation, err error) context.Context

func (*LoggingPlugin) OnLLMError

func (p *LoggingPlugin) OnLLMError(ctx context.Context, call *LLMCall, err error) context.Context

func (*LoggingPlugin) OnToolError

func (p *LoggingPlugin) OnToolError(ctx context.Context, call *ToolCall, err error) context.Context

type MagicDocUpdater

type MagicDocUpdater interface {
	UpdateIfNeeded(ctx context.Context, workDir string, changedFiles []string)
}

MagicDocUpdater is the interface for the auto-updating docs system. Defined here to avoid import cycles with the agent package.

type MagicDocsPlugin

type MagicDocsPlugin struct {
	// contains filtered or unexported fields
}

MagicDocsPlugin runs MagicDocs as a PostTurnHook on the final turn when files were modified during the session.

func NewMagicDocsPlugin

func NewMagicDocsPlugin(u MagicDocUpdater) *MagicDocsPlugin

NewMagicDocsPlugin creates a plugin wrapping the MagicDocUpdater.

func (*MagicDocsPlugin) AfterTurn

func (p *MagicDocsPlugin) AfterTurn(ctx context.Context, turn *TurnInfo)

AfterTurn implements PostTurnHooks. Only fires on the final turn when changed files exist.

func (*MagicDocsPlugin) Name

func (p *MagicDocsPlugin) Name() string

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager holds registered plugins and runs hooks in order. Thread-safe for concurrent hook execution (plugins are read-only after registration).

func NewManager

func NewManager(logger *slog.Logger) *Manager

NewManager creates a plugin manager.

func (*Manager) BudgetPlugin

func (m *Manager) BudgetPlugin() *BudgetPlugin

BudgetPlugin returns the registered BudgetPlugin, or nil if none is registered.

func (*Manager) Plugins

func (m *Manager) Plugins() []Plugin

Plugins returns a copy of the registered plugins (safe for inspection).

func (*Manager) Register

func (m *Manager) Register(p Plugin)

Register adds a plugin. Call before starting the agent loop. Panics on nil.

func (*Manager) RunAfterAgentRun

func (m *Manager) RunAfterAgentRun(ctx context.Context, inv *Invocation, result *RunResult) context.Context

RunAfterAgentRun executes AfterAgentRun on all plugins with AgentHooks.

func (*Manager) RunAfterLLMCall

func (m *Manager) RunAfterLLMCall(ctx context.Context, call *LLMCall, usage *TokenUsage) context.Context

RunAfterLLMCall executes AfterLLMCall on all plugins with LLMHooks.

func (*Manager) RunAfterToolCall

func (m *Manager) RunAfterToolCall(ctx context.Context, call *ToolCall, result *ToolResult) context.Context

RunAfterToolCall executes AfterToolCall on all plugins with ToolHooks.

func (*Manager) RunBeforeAgentRun

func (m *Manager) RunBeforeAgentRun(ctx context.Context, inv *Invocation) (context.Context, error)

RunBeforeAgentRun executes BeforeAgentRun on all plugins with AgentHooks. First error stops the chain and returns it.

func (*Manager) RunBeforeLLMCall

func (m *Manager) RunBeforeLLMCall(ctx context.Context, call *LLMCall) (context.Context, error)

RunBeforeLLMCall executes BeforeLLMCall on all plugins with LLMHooks.

func (*Manager) RunBeforeToolCall

func (m *Manager) RunBeforeToolCall(ctx context.Context, call *ToolCall) (context.Context, error)

RunBeforeToolCall executes BeforeToolCall on all plugins with ToolHooks.

func (*Manager) RunOnAgentError

func (m *Manager) RunOnAgentError(ctx context.Context, inv *Invocation, err error) context.Context

RunOnAgentError executes OnAgentError on all plugins with AgentHooks.

func (*Manager) RunOnLLMError

func (m *Manager) RunOnLLMError(ctx context.Context, call *LLMCall, err error) context.Context

RunOnLLMError executes OnLLMError on all plugins with LLMHooks.

func (*Manager) RunOnToolError

func (m *Manager) RunOnToolError(ctx context.Context, call *ToolCall, err error) context.Context

RunOnToolError executes OnToolError on all plugins with ToolHooks.

func (*Manager) RunPostTurn

func (m *Manager) RunPostTurn(ctx context.Context, turn *TurnInfo)

RunPostTurn executes AfterTurn on all plugins with PostTurnHooks.

func (*Manager) UsageStore

func (m *Manager) UsageStore() *UsageStore

UsageStore returns the UsageStore from the registered BudgetPlugin, or nil if no BudgetPlugin is registered or it has no UsageStore wired.

type ModelBreakdown

type ModelBreakdown struct {
	Model         string  `json:"model"`
	ProviderID    string  `json:"providerId"`
	TotalCalls    int     `json:"totalCalls"`
	SuccessCalls  int     `json:"successCalls"`
	ErrorCalls    int     `json:"errorCalls"`
	AvgDurationMs float64 `json:"avgDurationMs"` // 0 when no duration data
	LastUsed      string  `json:"lastUsed"`      // RFC3339 timestamp of most recent call
}

ModelBreakdown holds per-model aggregated stats.

type Plugin

type Plugin interface {
	Name() string
}

Plugin is the base interface. Implement only the hook interfaces you need.

type PostTurnHooks

type PostTurnHooks interface {
	// AfterTurn fires after each ReAct round. Called with the round number,
	// whether this is the final turn (session ending), and which files were
	// modified during the session so far.
	AfterTurn(ctx context.Context, turn *TurnInfo)
}

PostTurnHooks fires after each ReAct round that had tool calls, and once on session completion. Use for per-turn memory extraction, MagicDocs, prompt suggestions, or any service that needs to process agent output incrementally.

type ProviderBreakdown

type ProviderBreakdown struct {
	ProviderID      string  `json:"providerId"` // matches ...Id convention (e.g. sessionId, newTaskId)
	CostType        string  `json:"costType"`   // "metered", "flat_rate", or "free"
	TotalCalls      int     `json:"totalCalls"`
	InputTokens     int64   `json:"inputTokens"`
	OutputTokens    int64   `json:"outputTokens"`
	ReasoningTokens int64   `json:"reasoningTokens"` // chain-of-thought tokens (subset of OutputTokens; 0 for non-reasoning models)
	MeteredCostUSD  float64 `json:"meteredCostUsd"`  // cost_usd for metered rows; 0 for flat_rate/free — matches ...Usd convention
	P50DurationMs   int64   `json:"p50DurationMs"`   // P50 call latency in ms (0 when no duration data)
	P99DurationMs   int64   `json:"p99DurationMs"`   // P99 call latency in ms (0 when no duration data)
}

ProviderBreakdown holds aggregated usage for a single (provider, cost_type) pair in a time window.

type QualityError

type QualityError struct {
	Gate    string // "lint", "review"
	Message string
}

QualityError is returned by BeforeToolCall to block a tool with a quality message.

func (*QualityError) Error

func (e *QualityError) Error() string

type QualityPlugin

type QualityPlugin struct {
	// contains filtered or unexported fields
}

QualityPlugin runs automatic quality gates on agent tool calls:

  • AfterToolCall: runs tests after code file edits
  • BeforeToolCall: runs lint and optional LLM review before git commits

func NewQualityPlugin

func NewQualityPlugin(provider llm.Provider, reviewModel string, bus *eventbus.Bus, logger *slog.Logger) *QualityPlugin

NewQualityPlugin creates a quality gate plugin. provider and reviewModel can be nil/empty to disable LLM review.

func (*QualityPlugin) AfterToolCall

func (p *QualityPlugin) AfterToolCall(ctx context.Context, call *ToolCall, result *ToolResult) context.Context

AfterToolCall runs tests after code file edits and publishes results.

func (*QualityPlugin) BeforeToolCall

func (p *QualityPlugin) BeforeToolCall(ctx context.Context, call *ToolCall) (context.Context, error)

BeforeToolCall gates git commits: runs lint, optionally LLM review.

func (*QualityPlugin) Name

func (p *QualityPlugin) Name() string

type RunResult

type RunResult struct {
	Rounds     int
	ToolCalls  int
	DurationMs int64
}

RunResult carries agent run outcome.

type SkillHooksPlugin

type SkillHooksPlugin struct {
	// contains filtered or unexported fields
}

SkillHooksPlugin executes pre/post tool-use hooks defined in SKILL.md frontmatter. Hooks are stored per-session to support concurrent sessions with different active skills.

func NewSkillHooksPlugin

func NewSkillHooksPlugin(logger *slog.Logger) *SkillHooksPlugin

NewSkillHooksPlugin creates an empty skill hooks plugin.

func (*SkillHooksPlugin) AddHooks

func (p *SkillHooksPlugin) AddHooks(sessionID string, pre, post []tool.SkillHook)

AddHooks appends hooks from an activated skill for a session. Supports multiple active skills per session.

func (*SkillHooksPlugin) AfterToolCall

func (p *SkillHooksPlugin) AfterToolCall(ctx context.Context, call *ToolCall, _ *ToolResult) context.Context

AfterToolCall fires matching post-tool-use hooks.

func (*SkillHooksPlugin) BeforeToolCall

func (p *SkillHooksPlugin) BeforeToolCall(ctx context.Context, call *ToolCall) (context.Context, error)

BeforeToolCall fires matching pre-tool-use hooks.

func (*SkillHooksPlugin) ClearSession

func (p *SkillHooksPlugin) ClearSession(sessionID string)

ClearSession removes all hooks for a session (called on session end).

func (*SkillHooksPlugin) Name

func (p *SkillHooksPlugin) Name() string

func (*SkillHooksPlugin) OnToolError

func (p *SkillHooksPlugin) OnToolError(ctx context.Context, _ *ToolCall, _ error) context.Context

OnToolError is a no-op — hooks don't fire on errors.

type TokenUsage

type TokenUsage struct {
	InputTokens       int
	OutputTokens      int
	CachedInputTokens int // prompt tokens served from provider cache (0 = not reported or no cache)
	ReasoningTokens   int // chain-of-thought tokens within OutputTokens (0 = not reported)
	Model             string
	// NativeCostUSD is the exact cost reported by the provider (e.g. OpenRouter
	// usage.cost). Zero means the provider did not report a native cost; the
	// budget plugin falls back to llm.EstimateCost in that case.
	NativeCostUSD float64
}

TokenUsage from an LLM response.

type ToolCall

type ToolCall struct {
	Name      string
	Input     json.RawMessage
	WorkDir   string // agent's working directory (worktree path or ".")
	SessionID string // session that triggered this call
}

ToolCall carries tool execution context.

type ToolHooks

type ToolHooks interface {
	// BeforeToolCall fires before a tool executes. Return error to block.
	BeforeToolCall(ctx context.Context, call *ToolCall) (context.Context, error)
	// AfterToolCall fires after a tool completes successfully.
	AfterToolCall(ctx context.Context, call *ToolCall, result *ToolResult) context.Context
	// OnToolError fires when a tool fails.
	OnToolError(ctx context.Context, call *ToolCall, err error) context.Context
}

ToolHooks intercepts tool execution.

type ToolResult

type ToolResult struct {
	Output   string
	Duration time.Duration
}

ToolResult carries tool output.

type TurnInfo

type TurnInfo struct {
	SessionID    string
	Round        int
	Final        bool     // true on session completion (last turn)
	ToolCalls    int      // tools called this round (cumulative total when Final=true)
	ChangedFiles []string // files modified during the session (cumulative, sorted)
	WorkDir      string   // agent's working directory
	Mode         tool.Mode
}

TurnInfo carries context about the completed turn.

type UsageStore

type UsageStore struct {
	// contains filtered or unexported fields
}

UsageStore persists LLM usage to the database so metrics can be aggregated across processes (main server + task workers).

func NewUsageStore

func NewUsageStore(db *sql.DB) *UsageStore

NewUsageStore creates a usage store.

func (*UsageStore) AvgCostPerCall

func (s *UsageStore) AvgCostPerCall(ctx context.Context, model string, lookbackDays int, minSamples int) (float64, error)

AvgCostPerCall returns the average metered cost per LLM call for a given model over the past lookbackDays days. Returns 0 and no error when there are fewer than minSamples successful calls (not enough history to make a useful estimate). The empty-model wildcard ("") averages across all models.

func (*UsageStore) BreakdownByModel

func (s *UsageStore) BreakdownByModel(ctx context.Context) ([]ModelBreakdown, error)

BreakdownByModel returns per-model success/error/latency stats across all time. Results are sorted by total_calls DESC.

func (*UsageStore) BreakdownByProvider

func (s *UsageStore) BreakdownByProvider(ctx context.Context, from, to time.Time) ([]ProviderBreakdown, error)

BreakdownByProvider returns cost/calls/tokens grouped by (provider_id, cost_type) for the given time range. Rows with empty provider_id are grouped under "unknown". Results are ordered by metered cost DESC. P50/P99 latency is computed from duration_ms values (calls with duration_ms = 0 are excluded from percentile calc).

func (*UsageStore) Record

func (s *UsageStore) Record(ctx context.Context, providerID, model, costType string, inputTokens, outputTokens int, costUSD float64, durationMs int64, reasoningTokens int) error

Record writes a successful LLM call's usage to the database with status="ok". costType must be "metered", "flat_rate", or "free"; empty string is coerced to "metered". Any other value is rejected with an error so invalid billing types never reach the DB. durationMs is the wall-clock call duration in milliseconds (0 when not measured). reasoningTokens is the chain-of-thought token count from reasoning models (0 = not reported). For failed calls use RecordError; for full control over status/errorType use RecordWithStatus.

func (*UsageStore) RecordError

func (s *UsageStore) RecordError(ctx context.Context, providerID, model, costType, errorType string) error

RecordError writes a failed LLM call to the database with status="error". errorType is a short label: "429", "400", "timeout", "empty", or "other".

func (*UsageStore) RecordWithStatus

func (s *UsageStore) RecordWithStatus(ctx context.Context, providerID, model, costType string, inputTokens, outputTokens int, costUSD float64, durationMs int64, status, errorType string, reasoningTokens int) error

RecordWithStatus is the low-level writer used by Record and RecordError.

func (*UsageStore) ThisWeek

func (s *UsageStore) ThisWeek(ctx context.Context) (DailyTotals, error)

ThisWeek returns aggregated usage for the current UTC week (Monday start). TotalCalls and token counts include all cost_type values. CostUSD is metered-only so budget caps are not inflated by flat_rate/free inference.

func (*UsageStore) Today

func (s *UsageStore) Today(ctx context.Context) (DailyTotals, error)

Today returns aggregated usage for the current UTC day. TotalCalls and token counts include all cost_type values. CostUSD is metered-only so budget caps are not inflated by flat_rate/free inference.

func (*UsageStore) TodayByProvider

func (s *UsageStore) TodayByProvider(ctx context.Context) ([]ProviderBreakdown, error)

TodayByProvider returns per-(provider_id, cost_type) breakdown for the current UTC day. It is a convenience wrapper around BreakdownByProvider for today's window.

Directories

Path Synopsis
Package companion tracks notifications sent to the user to prevent duplicates across restarts and surfaces notification status in the feed UI.
Package companion tracks notifications sent to the user to prevent duplicates across restarts and surfaces notification status in the feed UI.
Package quality provides project-type-aware quality gates for agent coding sessions.
Package quality provides project-type-aware quality gates for agent coding sessions.

Jump to

Keyboard shortcuts

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