agent

package
v0.24.0 Latest Latest
Warning

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

Go to latest
Published: Aug 2, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const MarkerRunTimedOut = "(run timed out)"

MarkerRunTimedOut is the synthetic assistant-message text inserted when a run ends on its MaxRunDuration deadline before the model replied. Exported so callers that surface partial output can recognise and exclude it (it is a status marker, not real assistant text).

Variables

View Source
var ErrBudgetExceeded = &BudgetExceededError{}

ErrBudgetExceeded is a sentinel for errors.Is checks.

View Source
var ErrDoomLoop = errors.New("doom loop detected")

ErrDoomLoop is a sentinel for a run stopped because it repeated identical tool calls (a doom loop). The concrete error wraps it via fmt.Errorf("%w").

View Source
var ErrMaxTurnsExceeded = errors.New("max turns exceeded")

ErrMaxTurnsExceeded is a sentinel for a run that hit its MaxTurns cap. The concrete error wraps it (with the turn count) via fmt.Errorf("%w").

Functions

This section is empty.

Types

type Agent

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

Agent runs the core loop: prompt → LLM → tool calls → execute → repeat. It's a library — no I/O, no TUI, no filesystem opinions.

func New

func New(cfg AgentConfig) (*Agent, error)

New creates an Agent from config. Returns error if configuration is invalid. Call Run() to execute a prompt.

func (*Agent) Abort

func (a *Agent) Abort()

Abort cancels the current run.

func (*Agent) AppendMessage

func (a *Agent) AppendMessage(msg core.AgentMessage) error

AppendMessage appends a non-LLM message to the current conversation state. Used by the TUI to persist timeline events before the next user turn.

func (*Agent) CancelSteer

func (a *Agent) CancelSteer() []core.SteerItem

CancelSteer drops and returns all steer messages still queued for inter-step delivery. Used when the user pulls queued steers back into the input to edit them, so the agent doesn't also deliver the originals (double-delivery). Safe to call while running; already-delivered steers cannot be recalled.

func (*Agent) Compact

func (a *Agent) Compact(ctx context.Context, focus string) (*core.CompactionPayload, error)

Compact forces context compaction regardless of the auto-compaction threshold. Returns the compaction payload on success, nil if there was nothing to compact, or an error if the agent is running or compaction fails.

func (*Agent) CompactAt

func (a *Agent) CompactAt() int

CompactAt returns the current soft compaction threshold in tokens (0 = the default window-based behavior).

func (*Agent) CompactAtFloor

func (a *Agent) CompactAtFloor() int

CompactAtFloor returns the lowest threshold SetCompactAt will actually honor, in tokens. Reads the agent's own compaction settings rather than the package defaults, since a session may have been built with different reserve/keep values.

func (*Agent) CompactWithCheckpoint

func (a *Agent) CompactWithCheckpoint(ctx context.Context, checkpoint, focus string) (*core.CompactionPayload, error)

CompactWithCheckpoint is Compact with a mechanically appended ephemeral checkpoint. The checkpoint bypasses the summarizer so it cannot be omitted. focus is an optional caller instruction (from `/compact <focus>`) forwarded to the summarizer; empty for automatic compaction.

func (*Agent) CompactionEpoch

func (a *Agent) CompactionEpoch() int

CompactionEpoch returns the current compaction epoch.

func (*Agent) Drain

func (a *Agent) Drain(timeout time.Duration)

Drain waits until all in-flight events have been processed by subscribers, or timeout expires. Rarely needed — Send/Run auto-drain before returning. Kept for backward compatibility and special cases (e.g., mid-run flushes).

func (*Agent) DrainSteers

func (a *Agent) DrainSteers() []core.SteerItem

DrainSteers removes and returns all steer messages still queued for inter-step delivery. Used to hand queued messages to a new run when the operation that accepted them (e.g. a manual compaction) finishes without running the agent loop, which would otherwise leave them undelivered.

func (*Agent) DrainUntilBarrier

func (a *Agent) DrainUntilBarrier() []core.SteerItem

DrainUntilBarrier removes and returns the queued steers up to (but not including) the first barrier command. Used by the pump to start a new run with the steers that follow an executed command.

func (*Agent) Enqueue

func (a *Agent) Enqueue(msg string)

Enqueue queues a message for post-turn delivery. It will be processed after the current agent turn completes, triggering a new turn. Safe to call at any time.

func (*Agent) IsRunning

func (a *Agent) IsRunning() bool

IsRunning returns true if the agent is currently executing a Send/Run.

func (*Agent) LoadMessages

func (a *Agent) LoadMessages(msgs []core.AgentMessage) error

LoadMessages replaces the conversation history with the given messages. Used to restore a previous session. Returns error if the agent is running.

func (*Agent) LoadState

func (a *Agent) LoadState(msgs []core.AgentMessage, compactionEpoch int) error

LoadState replaces the full conversation state including compaction epoch. Used to restore a previous session with compaction history.

func (*Agent) MaxBudget

func (a *Agent) MaxBudget() float64

MaxBudget returns the current per-run budget ceiling in USD (0 = unlimited).

func (*Agent) Messages

func (a *Agent) Messages() []core.AgentMessage

Messages returns a shallow copy of the current conversation messages. The returned slice is independent (append-safe), but individual messages share content slices with the internal state. Safe for reading (e.g., JSON marshaling for session persistence) but callers should not mutate content.

func (*Agent) Model

func (a *Agent) Model() core.Model

Model returns the current model.

func (*Agent) NativeDocBytesUndelivered

func (a *Agent) NativeDocBytesUndelivered() int64

NativeDocBytesUndelivered returns the decoded native document/image bytes that have been accepted into the session (queued steers, plus any drained batch still in flight to history) but are not yet visible in the conversation history. The serve layer adds this to the history total when enforcing the per-session native-content budget, so concurrent sends can't collectively exceed it through the async steer-delivery window.

func (*Agent) PeekQueueHead

func (a *Agent) PeekQueueHead() (core.SteerItem, bool)

PeekQueueHead returns a copy of the item at the head of the unified queue without removing it, and false when the queue is empty. Used by the bus queue pump to decide whether the next item is a barrier command or a steer.

func (*Agent) PendingSteers

func (a *Agent) PendingSteers() []core.SteerItem

PendingSteers returns a snapshot of the user-visible steer messages still queued for inter-step delivery, without removing them. Used to report authoritative queue state (e.g. reconnect snapshots) without disturbing delivery order. Internal steers (system-generated, with suppressed delivery events) are excluded so they never surface as phantom "queued" chips.

func (*Agent) PermissionCheck

func (a *Agent) PermissionCheck() func(ctx context.Context, name string, args map[string]any) *core.ToolCallDecision

PermissionCheck returns the current permission callback.

func (*Agent) PopQueueBarrier

func (a *Agent) PopQueueBarrier(id string) bool

PopQueueBarrier removes the head item only if it is still the barrier command with the given ID, returning false when the head changed. Lets the pump execute a queued command exactly once, safely against concurrent enqueues.

func (*Agent) PushSteersFront

func (a *Agent) PushSteersFront(items []core.SteerItem)

PushSteersFront re-inserts items at the head of the steer queue, preserving their order. Used to hand drained items back when a delivery attempt loses a race for the run slot, so FIFO order (oldest first) survives.

func (*Agent) QueueLen

func (a *Agent) QueueLen() int

QueueLen returns the number of items currently in the unified queue rail (steers and barriers, including internal steers). Used by the producer-side strict-order gate: a user run must not start while the queue is non-empty.

func (*Agent) Reconfigure

func (a *Agent) Reconfigure(provider core.Provider, model core.Model, thinkingLevel string) error

Reconfigure swaps the provider, model, and/or thinking level mid-conversation. Preserves conversation history. Strips thinking blocks from historical assistant messages to avoid invalid signatures when the model changes. Returns error if the agent is currently running.

func (*Agent) ReleaseNativeDocBytes

func (a *Agent) ReleaseNativeDocBytes(n int64)

ReleaseNativeDocBytes undoes a ReserveNativeDocBytes when the reserved send never started (so SendWithContent's settle won't run).

func (*Agent) ReserveNativeDocBytes

func (a *Agent) ReserveNativeDocBytes(n int64)

ReserveNativeDocBytes adds n decoded native-content bytes to the inflight ledger BEFORE a direct content send's message reaches history. A direct SendWithContent appends to history asynchronously (in the run goroutine), so without this reservation a concurrent send could read the quota after the caller released its serialization lock but before the message is countable in history, and admit content past the per-session cap. The paired settle happens inside SendWithContent right after the append; if the send is never started (e.g. the run slot was lost), the caller must release the reservation with ReleaseNativeDocBytes.

func (*Agent) Reset

func (a *Agent) Reset() error

Reset clears conversation state. Returns error if the agent is currently running.

Reset deliberately does NOT drop the queued steers/barriers: when a queued /clear barrier is executed at idle, everything still in the queue is, by FIFO, behind the /clear and therefore belongs to the fresh conversation (reset in-place). Callers that want to discard the queue call CancelSteer explicitly.

func (*Agent) RestoreConversation

func (a *Agent) RestoreConversation(messages []core.AgentMessage, epoch int) error

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, prompt string) ([]core.AgentMessage, error)

Run initializes state with a new prompt and runs the agent loop. Any previous conversation state is replaced. For multi-turn, use Send. Returns all messages produced during the run. Before returning, Run waits for all accepted in-flight events to be processed by subscribers (up to DrainTimeout). Dropped events are not waited on.

func (*Agent) RunCost

func (a *Agent) RunCost() float64

RunCost returns the USD cost accumulated by the most recent Run/Send. It is a faithful measure of real spend whenever the model has pricing — including usage from empty or failed turns that never became an assistant message — regardless of whether a MaxBudget cap was active.

func (*Agent) Send

func (a *Agent) Send(ctx context.Context, prompt string) ([]core.AgentMessage, error)

Send appends a user message and runs the agent loop, continuing the conversation. If no previous state exists (e.g., first call without Run), state is auto-initialized. State mutation is atomic with the "not running" check — concurrent Send calls cannot corrupt state. Before returning, Send waits for all accepted in-flight events to be processed by subscribers (up to DrainTimeout). Dropped events are not waited on.

func (*Agent) SendItems

func (a *Agent) SendItems(ctx context.Context, items []core.SteerItem, msgIDs []string, announce func()) ([]core.AgentMessage, []string, error)

SendItems appends one user message per queued item (each with its own MsgID, carrying image/content blocks when present) and runs the agent loop. It is used to start a fresh run for the steers that were queued after a barrier command, preserving per-item granularity (no folding into one message).

msgIDs, when non-empty, supplies the stable MsgID for each item in order (len(msgIDs) must equal len(items)); an empty entry (or a nil/short slice) is auto-minted. Callers pre-mint so they can announce each delivered chip with a known MsgID without waiting for the run — clients dedup by MsgID on reconnect. The returned MsgIDs are the effective ones in item order. Barrier items are commands, never messages, and are skipped defensively.

announce, when non-nil, is invoked once the items are genuinely in history and before the run's own events — the same guarantee SendWithMsgID gives a direct prompt, so an announcement can never race a concurrent history snapshot (a reconnect between the two would lose the steer until a reload).

func (*Agent) SendPrepareCompact

func (a *Agent) SendPrepareCompact(ctx context.Context, prompt string, slot *sessioncheckpoint.Slot, extraPrompt string) ([]core.AgentMessage, error)

SendPrepareCompact runs the internal pre-compaction turn. It is the only entry point that grants the checkpoint permission bypass, and it always constructs that tool from slot rather than accepting a caller-provided tool.

func (*Agent) SendWithContent

func (a *Agent) SendWithContent(ctx context.Context, content []core.Content) ([]core.AgentMessage, error)

SendWithContent appends a user message with mixed content blocks (text + images) and runs the agent loop, continuing the conversation. The content is deep-cloned (core.CloneContent) to take ownership from the caller, so a later mutation of the caller's slice or of a block's Arguments map can't change the stored message or race a concurrent reader. Before returning, SendWithContent waits for all accepted in-flight events to be processed by subscribers (up to DrainTimeout). Dropped events are not waited on.

func (*Agent) SendWithContentAnnounced added in v0.21.0

func (a *Agent) SendWithContentAnnounced(ctx context.Context, content []core.Content, msgID string) ([]core.AgentMessage, error)

SendWithContentAnnounced is SendWithContentMsgID that also announces the prompt (AgentEventUserMessage) from the append point. Only the user-initiated ingress paths announce: internal producers render themselves.

func (*Agent) SendWithContentMsgID added in v0.21.0

func (a *Agent) SendWithContentMsgID(ctx context.Context, content []core.Content, msgID string) ([]core.AgentMessage, error)

SendWithContentMsgID is SendWithContent with a caller-supplied MsgID for the user message, letting the caller announce this prompt live under an ID shared with the message that lands in state — so clients dedup it against their optimistic echo and against reconnect snapshots. Mirrors SendWithMsgID.

func (*Agent) SendWithCustom

func (a *Agent) SendWithCustom(ctx context.Context, prompt string, custom map[string]any) ([]core.AgentMessage, error)

SendWithCustom appends a user message with custom metadata and runs the agent loop. The custom map is attached to the AgentMessage (persisted in session, available to frontends for rendering decisions) but does not affect LLM behavior.

func (*Agent) SendWithMsgID

func (a *Agent) SendWithMsgID(ctx context.Context, prompt, msgID string) ([]core.AgentMessage, error)

SendWithMsgID is Send with a caller-supplied MsgID for the user message, letting the caller correlate a later event (e.g. a Steered announcement for a batch of queued steers folded into this one prompt) with the message that lands in state — so reconnect snapshots dedup it by that shared MsgID.

It also announces the prompt (AgentEventUserMessage) from the append point, so subscribers only learn about it once it is genuinely in history — see announceUserMessage.

func (*Agent) SetCompactAt

func (a *Agent) SetCompactAt(tokens int) error

SetCompactAt sets the soft compaction threshold in tokens. When >0, the agent compacts once context exceeds this many tokens (clamped to the model window), instead of waiting for the full window. 0 restores the default (window-based) behavior. Returns error if the agent is currently running.

func (*Agent) SetMaxBudget

func (a *Agent) SetMaxBudget(v float64) error

SetMaxBudget changes the per-run budget ceiling. Goal mode uses this to cap each iteration at the remaining total budget. Validated like New(): a positive budget requires model pricing. Returns an error if the agent is running.

func (*Agent) SetModel

func (a *Agent) SetModel(provider core.Provider, model core.Model) error

SetModel changes the model and optionally the provider. If provider is nil, keeps the current provider. Strips thinking blocks from history when the model changes. Returns error if the agent is currently running.

func (*Agent) SetPermissionCheck

func (a *Agent) SetPermissionCheck(fn func(ctx context.Context, name string, args map[string]any) *core.ToolCallDecision) error

SetPermissionCheck swaps the permission check function at runtime. nil disables permission checks. Returns error if the agent is running.

func (*Agent) SetSystemPrompt

func (a *Agent) SetSystemPrompt(prompt string) error

SetSystemPrompt replaces the system prompt. Returns error if the agent is running.

func (*Agent) SetThinkingLevel

func (a *Agent) SetThinkingLevel(level string) error

SetThinkingLevel changes only the thinking level. Returns error if the agent is currently running.

func (*Agent) SnapshotConversation

func (a *Agent) SnapshotConversation() ([]core.AgentMessage, int)

func (*Agent) Steer

func (a *Agent) Steer(it core.SteerItem) bool

Steer queues a message for inter-step delivery. The agent sees it at the next gap between tool executions. Safe to call while running. Returns false if the queue is full (the message was dropped), so callers can surface a rejection instead of confirming a message that will never arrive.

func (*Agent) Subscribe

func (a *Agent) Subscribe(fn func(core.AgentEvent)) func()

Subscribe registers a listener for agent events. Returns an unsubscribe function. Listeners are async — slow listeners don't block the loop. Send/Run auto-drain before returning, so all accepted events are processed.

func (*Agent) SystemPrompt

func (a *Agent) SystemPrompt() string

SystemPrompt returns the current system prompt.

func (*Agent) ThinkingLevel

func (a *Agent) ThinkingLevel() string

ThinkingLevel returns the current thinking level.

func (*Agent) TimedOut

func (a *Agent) TimedOut() bool

TimedOut reports whether the most recent Run/Send ended because the run's own MaxRunDuration deadline tripped. False for a user abort, a provider error, or a normal completion. Derived from the run context, not the returned error.

type AgentConfig

type AgentConfig struct {
	Provider      core.Provider
	Model         core.Model
	SystemPrompt  string
	ThinkingLevel string
	CacheTTL      string // Prompt-cache TTL: "" (5m default) or "1h". Interactive agent only.
	MaxTokens     int    // Max output tokens per LLM call. 0 = shared model-aware default.
	Tools         *core.Registry
	Extensions    []extension.Extension
	WorkspaceRoot string

	// Guardrails
	MaxTurns            int           // Default: 50. 0 = unlimited.
	MaxToolCallsPerTurn int           // Default: 20. 0 = unlimited.
	MaxRunDuration      time.Duration // Default: 30m. 0 = unlimited.
	MaxBudget           float64       // Max USD per run. 0 = unlimited. Requires Model.Pricing when > 0.

	// Permission check called before each tool execution. May block waiting
	// for user approval. Return nil to approve, blocking decision to reject.
	// nil = no permission checks (all tools auto-approved).
	PermissionCheck func(ctx context.Context, name string, args map[string]any) *core.ToolCallDecision

	// Custom message conversion (nil = default: filter non-LLM messages)
	ConvertToLLM func([]core.AgentMessage) []core.Message

	// MaterializeContent, if set, is called with the LLM-ready messages just before
	// the provider request is built, to expand attachment descriptors into
	// provider-ready inline bytes on a COPY. It must not mutate its input. A
	// non-nil error aborts the turn (the referenced attachment could not be
	// resolved). pkg/agent stays decoupled from the blob store: the closure is
	// injected by the caller via the store's MaterializerFor.
	MaterializeContent func(context.Context, []core.Message) ([]core.Message, error)

	// Compaction settings. nil = use DefaultCompactionSettings.
	// Set Enabled:false to disable.
	Compaction *core.CompactionSettings

	// SessionCheckpoint is the ephemeral handoff slot. When set, automatic
	// compaction appends its contents to the summary and clears it, matching
	// the manual CompactWithCheckpoint path.
	SessionCheckpoint *sessioncheckpoint.Slot

	// DrainTimeout is the maximum time Send/Run will wait for subscribers to
	// finish processing events before returning. Default: 2s.
	// Set to 0 to disable auto-drain.
	DrainTimeout time.Duration

	Logger *slog.Logger
}

AgentConfig configures an Agent.

type AgentState

type AgentState struct {
	Messages        []core.AgentMessage
	Model           core.Model
	CompactionEpoch int // incremented after each compaction; invalidates stale Usage
}

AgentState holds the mutable state during an agent run.

type BudgetExceededError

type BudgetExceededError struct {
	Spent float64
	Limit float64
}

BudgetExceededError is returned when a run's accumulated cost exceeds MaxBudget.

func (*BudgetExceededError) Error

func (e *BudgetExceededError) Error() string

func (*BudgetExceededError) Is

func (e *BudgetExceededError) Is(target error) bool

Is reports whether target is a *BudgetExceededError, enabling errors.Is checks against the ErrBudgetExceeded sentinel.

type Emitter

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

Emitter fans out events to subscribers asynchronously. Each subscriber has a buffered channel. If the buffer fills, events are dropped. Panics in handlers are recovered without stranding the inflight counter.

Drain() waits until all accepted in-flight events have been processed. Dropped events (full buffer) are not tracked and won't delay Drain.

func NewEmitter

func NewEmitter(logger *slog.Logger) *Emitter

NewEmitter creates an emitter.

func (*Emitter) Drain

func (e *Emitter) Drain(timeout time.Duration)

Drain waits until all in-flight events have been processed by all subscribers, or timeout expires. The timeout is a safety net for stuck handlers.

func (*Emitter) Emit

func (e *Emitter) Emit(event core.AgentEvent)

Emit sends an event to all active subscribers. Lossy delta events are dropped when a subscriber's buffer is full; structural events block until there is room (or the subscriber is torn down) so they are never lost.

func (*Emitter) Subscribe

func (e *Emitter) Subscribe(fn func(core.AgentEvent)) func()

Subscribe registers a listener. Returns an unsubscribe function (safe to call multiple times). The listener runs in its own goroutine and receives events asynchronously.

type Hooks

type Hooks interface {
	FireBeforeAgentStart(ctx context.Context) []core.AgentMessage
	FireToolCall(ctx context.Context, name string, args map[string]any) *core.ToolCallDecision
	FireToolResult(ctx context.Context, name string, result core.Result, isError bool) core.Result
	FireContext(ctx context.Context, msgs []core.AgentMessage) []core.AgentMessage
	FireObserver(event core.AgentEvent)
}

Hooks is the interface the agent loop needs from the extension system. Defined here (consumer-side) so the loop doesn't depend on extension internals.

Jump to

Keyboard shortcuts

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