Documentation
¶
Overview ¶
Package agent is the orchestrator that wires session storage, provider streaming, permission gating, tool execution, and cost accounting into a single turn-by-turn loop.
Layering ¶
internal/agent depends on every package below it: bus, session, store, provider, permission, tool, cost. It is the keystone of the v0.1 architecture. It must NOT import internal/ui or cmd/...; those import it.
The Send loop in one paragraph ¶
Send appends the user message, then delegates the active turn to Fantasy when a Fantasy model is configured. Fantasy owns model/tool iteration and is intentionally uncapped: cancellation is context-driven. The legacy provider loop still honors Options.MaxIterations for nil-Fantasy test/fallback seams.
Sequential tool execution ¶
Even when the provider returns multiple tool_use blocks in a single response, the agent executes them one at a time. Permission prompts are interactive — stacking modals on top of each other is hostile. Parallel execution is a v0.2 concern. This is safe because the Anthropic tool-use protocol does not require any specific ordering of tool_result blocks within a tool_result message.
Per-session serialisation ¶
At most one Send is in flight per session ID. Concurrent Sends on the same session block on a per-session mutex; Sends on different sessions run independently. Compact participates in the same lock so it cannot race a Send on the same session.
Streaming-error and cancellation semantics ¶
A mid-stream provider.EventError commits a partial assistant message containing whatever text/thinking arrived before the error, plus a stream_error metadata flag, and returns the wrapped error to the caller. This is intentional: the model's partial output is interesting to the user, and the conversation can be inspected before retrying. Tool calls that arrived before the error are NOT executed — we want a clean failure boundary.
Context cancellation, by contrast, commits NOTHING. When ctx is cancelled mid-stream the agent returns ctx.Err immediately; no message is appended. Rationale: cancellation is the user's explicit signal that they don't want this turn — preserving a half-formed assistant message would undo that intent and pollute history.
Cost lookups are best-effort ¶
Pricing lookups go through the catalog. If the catalog returns cost.ErrModelNotPriced, the agent logs a slog.Warn and records the token usage with cost_usd = 0. A turn is never failed over pricing.
Index ¶
- Variables
- type Agent
- func (a *Agent) ClearQueue(sessionID string) int
- func (a *Agent) Close() error
- func (a *Agent) Compact(ctx context.Context, sessionID string) (*session.Marker, error)
- func (a *Agent) InjectMessage(ctx context.Context, pluginName, sessionID, role, content string) error
- func (a *Agent) IsSessionBusy(sessionID string) bool
- func (a *Agent) QueueCount(sessionID string) int
- func (a *Agent) QueuedPrompts(sessionID string) []string
- func (a *Agent) ResetPluginInjectCounters(sessionID string)
- func (a *Agent) Send(ctx context.Context, sessionID string, userParts []session.Part) (*session.Message, error)
- func (a *Agent) SetModel(providerName, modelName string, prv provider.Provider, ...) error
- type Options
- type QueuedSend
- type Runtime
- type RuntimeOptions
- type SessionAgent
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("agent: closed")
ErrClosed is returned by Send and Compact after Close.
var ErrInjectCap = errors.New("agent: plugin inject cap reached")
ErrInjectCap is returned by InjectMessage when a plugin has injected the maximum number of messages for the current turn.
var ErrIterationLimit = errors.New("agent: iteration limit reached")
ErrIterationLimit is returned by the legacy nil-Fantasy provider loop when it hits its configured iteration cap without converging. Fantasy active turns are uncapped.
var ErrNothingToCompact = errors.New("agent: nothing to compact")
ErrNothingToCompact is returned by Compact when the session contains too few messages since the latest marker to justify summarising.
Functions ¶
This section is empty.
Types ¶
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent is the orchestrator. Construct via New; the zero value is not usable.
func (*Agent) ClearQueue ¶
ClearQueue drops all pending queued sends for the session. Returns the number of sends that were dropped.
func (*Agent) Close ¶
Close releases the agent. After Close, Send and Compact return ErrClosed. Idempotent.
func (*Agent) Compact ¶
Compact summarises the session's pre-marker history and writes a new compaction marker. The agent itself generates the summary by calling the provider with a dedicated "summarise this conversation" prompt.
Returns ErrNothingToCompact if there are fewer than 4 messages since the latest marker — summarising 1–3 messages is not worth a provider round-trip. ErrNothingToCompact does NOT publish any events.
Bus events published in order:
- bus.CompactionStarted — before the provider call (always, unless ErrNothingToCompact is returned).
- bus.CompactionCompleted — after the marker is persisted (success).
- bus.CompactionFailed — if any error occurs after Started was published.
Compact is permission-free (it uses no tools) and serialises against Send on the same session through the per-session lock.
func (*Agent) InjectMessage ¶
func (a *Agent) InjectMessage(ctx context.Context, pluginName, sessionID, role, content string) error
InjectMessage appends a message to sessionID on behalf of a plugin.
role must be "user" or "assistant". Only "user" messages trigger a new agent turn; "assistant" messages are persisted but the loop is not re- entered (they serve as synthetic context injections).
Each plugin is tracked by pluginName. At most maxPluginInjectsPerTurn calls per pluginName per active turn are processed; additional calls return ErrInjectCap without appending anything.
func (*Agent) IsSessionBusy ¶
IsSessionBusy reports whether the session has an active run in flight.
func (*Agent) QueueCount ¶
QueueCount returns the number of pending queued sends for the session.
func (*Agent) QueuedPrompts ¶
QueuedPrompts returns the queued prompt texts (first PartText of each) for display in the UI.
func (*Agent) ResetPluginInjectCounters ¶
ResetPluginInjectCounters resets the per-turn injection counters for sessionID. Called by the agent loop at the start of each turn so the cap applies per-turn, not per-session.
func (*Agent) Send ¶
func (a *Agent) Send(ctx context.Context, sessionID string, userParts []session.Part) (*session.Message, error)
Send appends a user message to the session and runs the agent loop until the assistant produces a final response with no further tool calls. Fantasy-backed turns are uncapped; the legacy nil-Fantasy loop can still hit ErrIterationLimit. Returns the final committed assistant message.
If a Send is already in flight for the session, the new send is enqueued and nil is returned immediately. The caller can inspect the queue via QueueCount / QueuedPrompts. The queued send is dispatched automatically once the active run completes.
Bus events emitted, in order per iteration:
- bus.MessageAppended (user) once at start
- bus.AssistantTextDelta streamed
- bus.AssistantThinkingDelta streamed
- bus.MessageAppended (assistant) per iteration end
- bus.CostUpdated after each provider response
- bus.ContextUsageUpdated after each provider response
- bus.ToolCallRequested per tool call
- bus.ToolCallCompleted per tool call
- bus.MessageAppended (tool result) per tool call
- bus.IterationLimitReached if the legacy loop cap is hit
- bus.TurnCompleted after a successful turn
- bus.QueueChanged when queue depth changes
Permission asks and tool progress events come from the tools themselves while their Execute method runs.
func (*Agent) SetModel ¶
func (a *Agent) SetModel(providerName, modelName string, prv provider.Provider, fm fantasy.LanguageModel) error
SetModel hot-swaps the active provider and Fantasy model used by subsequent sends. It does not mutate existing session rows; callers own any UX or config persistence around the session-only runtime switch.
type Options ¶
type Options struct {
// Bus is the in-process event bus. Required.
Bus *bus.Bus
// Store is the session persistence layer. Required.
Store session.Store
// Provider is the model adapter. Required.
Provider provider.Provider
// FantasyModel, when non-nil, is used by the active turn loop via
// fantasy.Agent.Stream. Provider remains required for name/model metadata
// and legacy test seams.
FantasyModel fantasy.LanguageModel
// Permission is the permission engine the tools call into. Required.
Permission *permission.Engine
// Tools is the registry of callable tools. Required.
Tools *tool.Registry
// Catalog resolves model pricing. Required.
Catalog *cost.Catalog
// SystemPrompt is the optional system prompt sent on every turn.
SystemPrompt string
// MaxIterations bounds only the legacy nil-Fantasy provider loop. Fantasy
// active turns are uncapped. Zero means defaultMaxIterations (25) for legacy.
MaxIterations int
// Pwd is the working directory passed to tools via ExecContext. Empty
// means the tool helpers fall back to os.Getwd.
Pwd string
// Now is an injectable clock for bus event timestamps. Nil means time.Now.
Now func() time.Time
// ContextWindow is the model's maximum context size in tokens. When
// non-zero, [bus.ContextUsageUpdated.PctUsed] is computed against it.
ContextWindow int64
// CompactionMaxTokens caps the size of the generated summary in
// Compact. Zero means 1024.
CompactionMaxTokens int
// LazyContext, when non-nil, enables the per-tool-call subdir
// AGENTS.md / CLAUDE.md loader (see agentsmd.LazyTracker). Nil
// means the feature is off — the agent loop never injects subdir
// context. Tracker state is per-Agent, but the agent maintains
// a per-session pending-block buffer so multiple sessions
// sharing one Agent do not bleed context into each other (only
// the seen-dir set is shared, which is the intended behaviour
// for one workspace).
LazyContext *agentsmd.LazyTracker
// Reasoning is the session-scoped reasoning knob copied onto
// every [provider.Request] this agent issues. The zero value
// means "no reasoning" — adapters that support reasoning will
// not enable it. CLI / config plumb a [provider.Reasoning]
// into this field at bootstrap.
Reasoning provider.Reasoning
// Hooks, when non-nil, gates each turn through the hook
// framework (pre_message, pre_tool, post_tool, post_message).
// A nil Hooks means "no hooks" — the agent loop treats it as a
// no-op without any nil-deref risk.
Hooks *hook.Registry
// CompactionThresholdPct, when > 0, enables the advisory compaction
// suggestion. After each turn the agent checks context usage against
// this percentage. If usage is at or above the threshold and the flag
// has not already fired for this session × crossing, it publishes a
// [bus.CompactionRequested] with Source="threshold". Valid range 1–99;
// 0 disables the suggestion. Default 80 (supplied by cmd/hygge/cli
// from config.Compaction.ThresholdPct).
CompactionThresholdPct float64
}
Options configures an Agent. Bus, Store, Provider, Permission, Tools, and Catalog are required; the rest have sensible defaults.
type QueuedSend ¶
type QueuedSend struct {
// Parts is the user message content to send once the session is free.
Parts []session.Part
}
QueuedSend holds the payload for a user message that arrived while the session was already busy. The send is held in the per-session queue and dispatched automatically when the current run completes.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime owns the model/tool assembly for active agent turns. It is intentionally narrower than Agent: Agent remains the external compatibility surface for UI/CLI callers, while Runtime decides which turn runner to use and how Fantasy tools are adapted from tool.Registry.
func NewRuntime ¶
func NewRuntime(opts RuntimeOptions) *Runtime
NewRuntime constructs the turn runtime. Tools may be nil only in tests that do not execute turns; Agent.New validates the production path first.
func (*Runtime) GenerateTitle ¶
func (r *Runtime) GenerateTitle(ctx context.Context, prompt string, maxTokens int) (string, provider.Usage, error)
GenerateTitle is the narrow no-tool seam for future model-generated session titles/slugs. Hygge currently displays FirstMessagePreview or a user-edited Slug, so this is intentionally not wired into UI/store mutation yet.
func (*Runtime) SetModel ¶
func (r *Runtime) SetModel(model fantasy.LanguageModel)
SetModel replaces the Fantasy language model used to create future agents. Existing fantasy.Agent instances are per-turn and are not reused, so assigning here is enough to invalidate the old model for subsequent sends and internal compaction/title calls.
type RuntimeOptions ¶
type RuntimeOptions struct {
Model fantasy.LanguageModel
Tools *tool.Registry
// MaxIterations is accepted for legacy construction compatibility. Fantasy
// active turns are intentionally uncapped; cancellation is context-driven.
MaxIterations int
}
RuntimeOptions configures Runtime.
type SessionAgent ¶
type SessionAgent struct {
// contains filtered or unexported fields
}
SessionAgent owns per-session turn execution. Queueing and busy-state live on Agent for API compatibility; this type is the handoff point for the eventual Phase 5/6 migration of the rest of the session lifecycle.
func NewSessionAgent ¶
func NewSessionAgent(agent *Agent, runtime *Runtime) *SessionAgent
NewSessionAgent wires an Agent-compatible session runner.