Documentation
¶
Overview ¶
Package agent is a model-agnostic coding-agent runtime in Go.
It turns a stream of LLM events into a stream of tool calls, with retries, steering, follow-ups, abort, and thinking-budget clamping already correct. Callers supply a model, a tool set, a prompt, and a streaming function; the agent emits structured events the host can render however it wants.
Package agent is intentionally small. It does not depend on a session store, MCP runtime, TUI, extension host, provider catalog, or any HTTP client. Its only dependencies are github.com/kfet/ai (the portable AI primitives: Message, Tool, Model, Usage, Context, streaming event types), github.com/kfet/pinexec (the bash tool's shell runner), and a couple of golang.org/x/ packages used by the image/path tools.
Headline API ¶
The simplest path is Agent:
a := agent.NewAgent(agent.AgentOptions{
InitialState: &agent.AgentState{Model: model},
StreamFn: myStreamFn, // any provider client
Tools: agent.ToolSetFrom(tools),
ConvertToLLM: agent.DefaultConvertToLLM,
})
unsubscribe := a.Subscribe(func(ev agent.AgentEvent) {
// render ev however you like
})
defer unsubscribe()
_ = a.Prompt("Refactor handler.go to use the new config struct.")
a.WaitForIdle()
For non-streaming one-shot use cases, Agent.SimplePrompt returns the assistant's final text without spinning up the full loop.
StreamFn ¶
The agent does not know how to talk to a specific provider. Callers pass a StreamFn — any function that, given a model, prompt context, and options, returns an github.com/kfet/ai.AssistantMessageEventStream. When the per-call StreamFn is nil, the agent falls back to DefaultStreamFn, a package-level factory hook that host applications install to wire up their own provider registry. Setting neither yields a clear "no stream function configured" error.
Tools ¶
Tools are AgentTool values; collect them in a ToolSet. Each tool is a name, a JSON schema, an executor, and optional display hints. The standard coding toolbox lives in github.com/kfet/agent/tools.
Thinking levels ¶
The canonical ladder is max → xhigh → high → medium → low → minimal → off. ClampThinkingLevel walks a requested level down to whatever the model actually supports. Knowledge about which specific model IDs support which levels lives outside this package — the host computes the available set and passes it in.
Concurrency ¶
Agent is safe for concurrent use by its event subscribers and by callers issuing Prompt/Steer/FollowUp/Abort. Internal state is guarded by a single mutex; subscribers are dispatched synchronously from the agent's goroutine, so subscribers must not block.
Example ¶
Example demonstrates the headline path: create an Agent, subscribe to its events, send a prompt, wait for it to finish.
package main
import (
"fmt"
"sync"
"time"
"github.com/kfet/agent"
"github.com/kfet/ai"
)
// fakeStreamFn returns a StreamFn that replays the given assistant
// messages, one per call, looping on the last response after the
// canned list is exhausted.
func fakeStreamFn(responses ...*ai.AssistantMessage) agent.StreamFn {
var mu sync.Mutex
idx := 0
return func(_ *ai.Model, _ ai.Context, _ *ai.SimpleStreamOptions) *ai.AssistantMessageEventStream {
mu.Lock()
msg := responses[idx]
if idx < len(responses)-1 {
idx++
}
mu.Unlock()
s := ai.NewAssistantMessageEventStream()
go func() {
s.Push(ai.AssistantMessageEvent{Type: ai.EventStart, Partial: msg})
s.Push(ai.AssistantMessageEvent{Type: ai.EventDone, Reason: msg.StopReason, Message: msg})
s.End(nil)
}()
return s
}
}
// exampleModel returns a Model wired up for Anthropic Messages so the
// examples compile against a realistic shape. No HTTP is involved —
// the StreamFn is faked.
func exampleModel() *ai.Model {
return &ai.Model{
ID: "example-model",
Name: "Example Model",
API: ai.APIAnthropicMessages,
Provider: ai.ProviderAnthropic,
ContextWindow: 200000,
MaxTokens: 4096,
}
}
func textResponse(text string) *ai.AssistantMessage {
return &ai.AssistantMessage{
Role: ai.RoleAssistant,
Content: []ai.AssistantContent{ai.NewTextContent(text)},
API: ai.APIAnthropicMessages,
Provider: ai.ProviderAnthropic,
Model: "example-model",
StopReason: ai.StopReasonStop,
Timestamp: time.Now().UnixMilli(),
}
}
func main() {
a := agent.NewAgent(agent.AgentOptions{
Model: exampleModel(),
StreamFn: fakeStreamFn(textResponse("Hello, world.")),
})
var got string
var mu sync.Mutex
unsubscribe := a.Subscribe(func(ev agent.AgentEvent) {
if ev.Type != agent.EventMessageEnd || ev.Message == nil {
return
}
if text := ev.Message.Text(); text != "" {
mu.Lock()
got = text
mu.Unlock()
}
})
defer unsubscribe()
if err := a.Prompt("hi"); err != nil {
fmt.Println("prompt error:", err)
return
}
a.WaitForIdle()
mu.Lock()
defer mu.Unlock()
fmt.Println(got)
}
Output: Hello, world.
Index ¶
- Constants
- Variables
- func DefaultConvertToLLM(messages []AgentMessage) ([]ai.Message, error)
- func IsCanonicalThinkingLevel(l ThinkingLevel) bool
- func ToAIThinkingLevel(t ThinkingLevel) ai.ThinkingLevel
- func ValidPriority(p string) bool
- func ValidStatus(s string) bool
- type Agent
- func (a *Agent) Abort()
- func (a *Agent) AppendMessage(m AgentMessage)
- func (a *Agent) ClearAllQueues()
- func (a *Agent) ClearFollowUpQueue()
- func (a *Agent) ClearMessages()
- func (a *Agent) ClearSteeringQueue()
- func (a *Agent) Continue() error
- func (a *Agent) FollowUp(m AgentMessage)
- func (a *Agent) FollowUpQueueLen() int
- func (a *Agent) GetAndClearFollowUpQueue() []AgentMessage
- func (a *Agent) GetFollowUpMode() string
- func (a *Agent) GetMaxRetryDelayMs() *int
- func (a *Agent) GetSessionID() string
- func (a *Agent) GetSteeringMode() string
- func (a *Agent) GetThinkingBudgets() *ai.ThinkingBudgets
- func (a *Agent) GetTransport() ai.Transport
- func (a *Agent) HasQueuedMessages() bool
- func (a *Agent) IdleChan() <-chan struct{}
- func (a *Agent) PeekFollowUpQueue() []AgentMessage
- func (a *Agent) Prompt(input string) error
- func (a *Agent) PromptMessages(messages []AgentMessage) error
- func (a *Agent) RemoveFollowUp(index int) (AgentMessage, bool)
- func (a *Agent) ReplaceMessages(msgs []AgentMessage)
- func (a *Agent) Reset()
- func (a *Agent) SetCompaction(c *ai.AnthropicCompaction)
- func (a *Agent) SetFollowUpMode(mode string)
- func (a *Agent) SetMaxRetryDelayMs(ms *int)
- func (a *Agent) SetModel(m *ai.Model)
- func (a *Agent) SetServerTools(tools []ai.AnthropicServerTool)
- func (a *Agent) SetSessionID(id string)
- func (a *Agent) SetSteeringMode(mode string)
- func (a *Agent) SetStreamFn(fn StreamFn)
- func (a *Agent) SetSystemPrompt(prompt string)
- func (a *Agent) SetThinkingBudgets(tb *ai.ThinkingBudgets)
- func (a *Agent) SetThinkingLevel(level ThinkingLevel)
- func (a *Agent) SetTransport(t ai.Transport)
- func (a *Agent) SimplePrompt(ctx context.Context, messages []AgentMessage, opts *SimplePromptOptions) (string, error)
- func (a *Agent) SimplePromptStream(ctx context.Context, messages []AgentMessage, opts *SimplePromptOptions, ...) (string, *ai.AssistantMessage, error)
- func (a *Agent) State() AgentState
- func (a *Agent) Steer(m AgentMessage)
- func (a *Agent) Subscribe(fn func(AgentEvent)) func()
- func (a *Agent) UpdateTools(fn func(ts *ToolSet))
- func (a *Agent) WaitForIdle()
- type AgentContext
- type AgentEvent
- type AgentEventType
- type AgentLoopConfig
- type AgentMessage
- func AgentLoop(ctx context.Context, prompts []AgentMessage, agentCtx *AgentContext, ...) []AgentMessage
- func AgentLoopContinue(ctx context.Context, agentCtx *AgentContext, config *AgentLoopConfig, ...) ([]AgentMessage, error)
- func NewAgentMessage(msg ai.Message) AgentMessage
- func StripUnmatchedToolCalls(msgs []AgentMessage) []AgentMessage
- type AgentOptions
- type AgentState
- type AgentTool
- type AgentToolResult
- type AgentToolUpdateCallback
- type BlockSummary
- type PlanEntry
- type PlanEntryPriority
- type PlanEntryStatus
- type ShouldStopAfterTurnContext
- type SimplePromptOptions
- type StreamFn
- type ThinkingLevel
- type TitleArg
- type ToolClassification
- type ToolDisplayHint
- type ToolSet
- func (ts *ToolSet) Add(t AgentTool)
- func (ts *ToolSet) ClassifyTools(extensionTools map[string][]string) ToolClassification
- func (ts *ToolSet) Clone() *ToolSet
- func (ts *ToolSet) Get(name string) (AgentTool, bool)
- func (ts *ToolSet) Has(name string) bool
- func (ts *ToolSet) Len() int
- func (ts *ToolSet) Names() []string
- func (ts *ToolSet) Remove(name string)
- func (ts *ToolSet) Slice() []AgentTool
Examples ¶
Constants ¶
const ( ThinkingOff = ai.ThinkingOff ThinkingMinimal = ai.ThinkingMinimal ThinkingLow = ai.ThinkingLow ThinkingMedium = ai.ThinkingMedium ThinkingHigh = ai.ThinkingHigh ThinkingXHigh = ai.ThinkingXHigh ThinkingMax = ai.ThinkingMax )
Re-export ai.ThinkingLevel constants for convenience.
const AutoResumeMarker = "▶"
AutoResumeMarker is the single-symbol user message the agent loop injects to auto-resume an assistant turn that was killed by a transport/stream error (e.g. "connection reset by peer" mid-stream) rather than a clean stop or tool call. The "play" triangle is an unambiguous, documented signal that the agent resumed the turn automatically — NOT real human input — mapping onto the situation: the turn was paused by the reset, and this presses play to resume it. U+25B6 (no variation selector) is a single code point that renders reliably across terminals, log files, and JSON transcripts.
Variables ¶
var CanonicalThinkingLadder = []ThinkingLevel{ ThinkingMax, ThinkingXHigh, ThinkingHigh, ThinkingMedium, ThinkingLow, ThinkingMinimal, ThinkingOff, }
CanonicalThinkingLadder lists thinking levels from highest to lowest. ClampThinkingLevel walks down this ladder when the requested level is not available for a given model.
var DefaultStreamFn func(ctx context.Context) StreamFn
DefaultStreamFn is consulted when an Agent's per-instance StreamFn is nil. Host applications install a factory here that closes over their provider registry; external consumers of this package either pass StreamFn explicitly or set DefaultStreamFn themselves.
The factory receives the call-site context.Context so the closure it returns can thread cancellation through to the provider stream.
A nil DefaultStreamFn plus a nil per-call StreamFn yields a "no stream function configured" error from Prompt / SimplePrompt.
Functions ¶
func DefaultConvertToLLM ¶
func DefaultConvertToLLM(messages []AgentMessage) ([]ai.Message, error)
DefaultConvertToLLM keeps only LLM-compatible messages.
func IsCanonicalThinkingLevel ¶
func IsCanonicalThinkingLevel(l ThinkingLevel) bool
IsCanonicalThinkingLevel reports whether l is one of the recognised thinking levels (off, minimal, low, medium, high, xhigh, max).
func ToAIThinkingLevel ¶
func ToAIThinkingLevel(t ThinkingLevel) ai.ThinkingLevel
ToAIThinkingLevel converts a ThinkingLevel to the ai-layer value. Returns empty string for "off" (off means no thinking).
func ValidPriority ¶
ValidPriority reports whether p is a known plan entry priority.
func ValidStatus ¶
ValidStatus reports whether s is a known plan entry status.
Types ¶
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent orchestrates the agent loop with state management and event dispatch.
func NewAgent ¶
func NewAgent(opts AgentOptions) *Agent
NewAgent creates a new Agent with the given options.
func (*Agent) AppendMessage ¶
func (a *Agent) AppendMessage(m AgentMessage)
AppendMessage appends a message.
func (*Agent) ClearAllQueues ¶
func (a *Agent) ClearAllQueues()
ClearAllQueues clears both steering and follow-up queues.
func (*Agent) ClearFollowUpQueue ¶
func (a *Agent) ClearFollowUpQueue()
ClearFollowUpQueue clears the follow-up queue.
func (*Agent) ClearSteeringQueue ¶
func (a *Agent) ClearSteeringQueue()
ClearSteeringQueue clears the steering queue.
func (*Agent) FollowUp ¶
func (a *Agent) FollowUp(m AgentMessage)
FollowUp queues a follow-up message for after the agent finishes.
func (*Agent) FollowUpQueueLen ¶
FollowUpQueueLen returns the number of queued follow-up messages.
func (*Agent) GetAndClearFollowUpQueue ¶
func (a *Agent) GetAndClearFollowUpQueue() []AgentMessage
GetAndClearFollowUpQueue atomically returns and clears the follow-up queue.
func (*Agent) GetFollowUpMode ¶
GetFollowUpMode returns the current follow-up mode.
func (*Agent) GetMaxRetryDelayMs ¶
GetMaxRetryDelayMs returns the current max retry delay.
func (*Agent) GetSessionID ¶
GetSessionID returns the current session ID.
func (*Agent) GetSteeringMode ¶
GetSteeringMode returns the current steering mode.
func (*Agent) GetThinkingBudgets ¶
func (a *Agent) GetThinkingBudgets() *ai.ThinkingBudgets
GetThinkingBudgets returns the current thinking budgets.
func (*Agent) GetTransport ¶
GetTransport returns the current preferred transport.
func (*Agent) HasQueuedMessages ¶
HasQueuedMessages returns true if there are any queued messages.
func (*Agent) IdleChan ¶
func (a *Agent) IdleChan() <-chan struct{}
IdleChan returns a channel that is closed when the agent is idle (not currently processing a prompt). Unlike WaitForIdle it composes with select, so callers can race agent idleness against their own cancellation or timeout:
select {
case <-a.IdleChan():
case <-ctx.Done():
}
A freshly-created agent that has never run reads as idle (the returned channel is already closed). While a run is in flight the channel is open and is closed when the run completes.
func (*Agent) PeekFollowUpQueue ¶
func (a *Agent) PeekFollowUpQueue() []AgentMessage
PeekFollowUpQueue returns a snapshot of the follow-up queue without modifying it.
func (*Agent) PromptMessages ¶
func (a *Agent) PromptMessages(messages []AgentMessage) error
PromptMessages sends agent messages as a prompt.
func (*Agent) RemoveFollowUp ¶
func (a *Agent) RemoveFollowUp(index int) (AgentMessage, bool)
RemoveFollowUp removes and returns the message at the given 0-based index. Returns the message and true if found, zero value and false otherwise.
func (*Agent) ReplaceMessages ¶
func (a *Agent) ReplaceMessages(msgs []AgentMessage)
ReplaceMessages replaces all messages.
func (*Agent) Reset ¶
func (a *Agent) Reset()
Reset clears all state except system prompt and model.
func (*Agent) SetCompaction ¶
func (a *Agent) SetCompaction(c *ai.AnthropicCompaction)
SetCompaction updates the Anthropic server-side compaction settings.
func (*Agent) SetFollowUpMode ¶
SetFollowUpMode sets the follow-up mode.
func (*Agent) SetMaxRetryDelayMs ¶
SetMaxRetryDelayMs sets the max retry delay.
func (*Agent) SetServerTools ¶
func (a *Agent) SetServerTools(tools []ai.AnthropicServerTool)
SetServerTools updates the Anthropic server-side tools (web search, code execution, etc.).
func (*Agent) SetSessionID ¶
SetSessionID sets the session ID for provider caching.
func (*Agent) SetSteeringMode ¶
SetSteeringMode sets the steering mode.
func (*Agent) SetStreamFn ¶
SetStreamFn overrides the stream function used for LLM calls.
func (*Agent) SetSystemPrompt ¶
SetSystemPrompt sets the system prompt.
func (*Agent) SetThinkingBudgets ¶
func (a *Agent) SetThinkingBudgets(tb *ai.ThinkingBudgets)
SetThinkingBudgets sets custom thinking budgets.
func (*Agent) SetThinkingLevel ¶
func (a *Agent) SetThinkingLevel(level ThinkingLevel)
SetThinkingLevel sets the thinking level.
func (*Agent) SetTransport ¶
SetTransport sets the preferred transport.
func (*Agent) SimplePrompt ¶
func (a *Agent) SimplePrompt(ctx context.Context, messages []AgentMessage, opts *SimplePromptOptions) (string, error)
SimplePrompt makes a single-turn LLM call with the given messages. See SimplePromptStream for the full contract — this is a thin wrapper that drops streaming events on the floor.
NO-COMPACTION CONTRACT: SimplePrompt MUST NOT trigger auto-compaction, ever. See SimplePromptStream's contract for the same guarantee.
Example ¶
ExampleAgent_SimplePrompt shows the non-streaming one-shot variant that just returns the final assistant text. Useful for batch jobs or for embedding the agent inside a larger non-interactive flow.
package main
import (
"context"
"fmt"
"sync"
"time"
"github.com/kfet/agent"
"github.com/kfet/ai"
)
// fakeStreamFn returns a StreamFn that replays the given assistant
// messages, one per call, looping on the last response after the
// canned list is exhausted.
func fakeStreamFn(responses ...*ai.AssistantMessage) agent.StreamFn {
var mu sync.Mutex
idx := 0
return func(_ *ai.Model, _ ai.Context, _ *ai.SimpleStreamOptions) *ai.AssistantMessageEventStream {
mu.Lock()
msg := responses[idx]
if idx < len(responses)-1 {
idx++
}
mu.Unlock()
s := ai.NewAssistantMessageEventStream()
go func() {
s.Push(ai.AssistantMessageEvent{Type: ai.EventStart, Partial: msg})
s.Push(ai.AssistantMessageEvent{Type: ai.EventDone, Reason: msg.StopReason, Message: msg})
s.End(nil)
}()
return s
}
}
// exampleModel returns a Model wired up for Anthropic Messages so the
// examples compile against a realistic shape. No HTTP is involved —
// the StreamFn is faked.
func exampleModel() *ai.Model {
return &ai.Model{
ID: "example-model",
Name: "Example Model",
API: ai.APIAnthropicMessages,
Provider: ai.ProviderAnthropic,
ContextWindow: 200000,
MaxTokens: 4096,
}
}
func textResponse(text string) *ai.AssistantMessage {
return &ai.AssistantMessage{
Role: ai.RoleAssistant,
Content: []ai.AssistantContent{ai.NewTextContent(text)},
API: ai.APIAnthropicMessages,
Provider: ai.ProviderAnthropic,
Model: "example-model",
StopReason: ai.StopReasonStop,
Timestamp: time.Now().UnixMilli(),
}
}
func main() {
a := agent.NewAgent(agent.AgentOptions{
Model: exampleModel(),
StreamFn: fakeStreamFn(textResponse("42")),
})
out, err := a.SimplePrompt(context.Background(), []agent.AgentMessage{
{Message: ai.NewUserMsg("What is the answer?", time.Now().UnixMilli())},
}, nil)
if err != nil {
fmt.Println("simple prompt error:", err)
return
}
fmt.Println(out)
}
Output: 42
func (*Agent) SimplePromptStream ¶
func (a *Agent) SimplePromptStream(ctx context.Context, messages []AgentMessage, opts *SimplePromptOptions, onEvent func(AgentEvent)) (string, *ai.AssistantMessage, error)
SimplePromptStream makes a single-turn LLM call with the given messages and forwards each agent event to onEvent as it is emitted. Behavior is otherwise identical to SimplePrompt:
- Reuses the agent's model, streamFn, api key resolution, and transport config but sends no tools, runs no agent loop, and does not modify the agent's state. The caller provides the full message list.
- Safe to call concurrently while the agent loop is running.
onEvent may be nil — events are then discarded. Callbacks are invoked synchronously on the same goroutine that drains the stream, so callers must keep their work cheap (the next event blocks until the callback returns).
Returns the rendered text, the final assistant message (or nil on error), and any error. On "no usable content" the error string includes a per-block summary so callers can diagnose redacted/empty responses without losing the raw message.
NO-COMPACTION CONTRACT: SimplePromptStream MUST NOT trigger auto-compaction. This is guaranteed by two design choices that must be preserved:
- The AgentLoopConfig built here intentionally omits the Compaction field, so no server-side compaction is requested.
- The events channel is a private, local channel drained synchronously by this function — events never reach AgentSession.checkAutoCompaction.
Do not forward these events to the session or add Compaction to the config.
func (*Agent) State ¶
func (a *Agent) State() AgentState
State returns the current agent state. The caller should not modify it.
func (*Agent) Steer ¶
func (a *Agent) Steer(m AgentMessage)
Steer queues a steering message to interrupt the agent mid-run.
func (*Agent) Subscribe ¶
func (a *Agent) Subscribe(fn func(AgentEvent)) func()
Subscribe registers an event listener. Returns an unsubscribe function.
func (*Agent) UpdateTools ¶
UpdateTools applies fn to the agent's ToolSet under the agent lock. This is the safe way to mutate tools — the callback sees the current state and all changes are atomic. No stale snapshots, no clobbering.
func (*Agent) WaitForIdle ¶
func (a *Agent) WaitForIdle()
WaitForIdle blocks until the agent finishes processing. It is a thin convenience wrapper around IdleChan.
type AgentContext ¶
type AgentContext struct {
SystemPrompt string
Messages []AgentMessage
Tools *ToolSet
}
AgentContext is like ai.Context but uses AgentTool.
type AgentEvent ¶
type AgentEvent struct {
Type AgentEventType
// For agent_end
Messages []AgentMessage
// For turn_end
TurnMessage *AgentMessage
ToolResults []ai.ToolResultMessage
// For message_start, message_update, message_end
Message *AgentMessage
// For message_update
AssistantMessageEvent *ai.AssistantMessageEvent
// For tool_execution_start, tool_execution_update, tool_execution_end
ToolCallID string
ToolName string
Args any
DisplayHint *ToolDisplayHint
// For tool_execution_update
PartialResult any
StatusMessage string // progress message from extensions (e.g. "Calling Read...")
// For tool_execution_end
Result any
IsError bool
// For stream_retry
RetryAttempt int // 1-based attempt number of the upcoming retry
ErrorMessage string // the stream error that triggered the retry
}
AgentEvent represents a lifecycle event from the agent.
type AgentEventType ¶
type AgentEventType string
AgentEventType identifies the type of agent lifecycle event.
const ( EventAgentStart AgentEventType = "agent_start" EventAgentEnd AgentEventType = "agent_end" EventTurnStart AgentEventType = "turn_start" EventTurnEnd AgentEventType = "turn_end" EventMessageStart AgentEventType = "message_start" EventMessageUpdate AgentEventType = "message_update" EventMessageEnd AgentEventType = "message_end" EventToolExecutionStart AgentEventType = "tool_execution_start" EventToolExecutionUpdate AgentEventType = "tool_execution_update" EventToolExecutionEnd AgentEventType = "tool_execution_end" // EventStreamRetry is emitted when the agent loop detects a mid-tool-call // stream error (stop_reason=error with an incomplete tool_use block whose // Arguments never finished streaming) and is about to retry the request // after dropping the broken partial from history. EventStreamRetry AgentEventType = "stream_retry" // EventAutoResume is emitted when an assistant turn ends with a transport/ // stream error (connection reset, broken pipe, unexpected EOF, …) rather // than a clean stop or tool call, and the agent loop is auto-resuming the // turn instead of pausing for a human. RetryAttempt is the 1-based resume // number and ErrorMessage is the transport error that triggered it. When a // partial response had already been emitted, the resume injects the // AutoResumeMarker user message so the model continues cleanly. EventAutoResume AgentEventType = "auto_resume" )
type AgentLoopConfig ¶
type AgentLoopConfig struct {
ai.SimpleStreamOptions
// Model is the LLM model to use.
Model *ai.Model
// ConvertToLLM converts AgentMessages to LLM-compatible Messages before each call.
ConvertToLLM func(messages []AgentMessage) ([]ai.Message, error)
// TransformContext is an optional transform applied before ConvertToLLM.
// Use for context window management, injecting external context, etc.
TransformContext func(ctx context.Context, messages []AgentMessage) ([]AgentMessage, error)
// GetAPIKey resolves an API key dynamically for each LLM call.
// Useful for short-lived OAuth tokens that may expire during tool execution.
GetAPIKey func(provider string) (string, error)
// GetSteeringMessages returns steering messages to inject mid-run.
// Called after the current assistant turn finishes executing its tool calls,
// unless ShouldStopAfterTurn exits first.
// Tool calls from the current assistant message are not skipped.
//
// Contract: must not return an error. Return nil/empty when no steering messages are available.
GetSteeringMessages func() ([]AgentMessage, error)
// ShouldStopAfterTurn is called after each turn fully completes and the
// turn_end event has been emitted. If it returns true, the loop emits
// agent_end and exits before polling steering or follow-up queues, without
// starting another LLM call.
//
// Use this to request a graceful stop after the current turn, e.g. before
// context gets too full.
//
// Contract: must not panic. Panicking interrupts the agent loop without
// producing a normal event sequence.
ShouldStopAfterTurn func(ctx ShouldStopAfterTurnContext) bool
// GetFollowUpMessages returns follow-up messages after the agent would otherwise stop.
GetFollowUpMessages func() ([]AgentMessage, error)
// Reasoning specifies the thinking/reasoning level.
Reasoning ai.ThinkingLevel
// SessionID is the unique identifier for this session.
SessionID string
// ThinkingBudgets specifies token budgets for thinking.
ThinkingBudgets *ai.ThinkingBudgets
// Transport is the preferred transport for providers that support multiple transports.
Transport ai.Transport
// MaxRetryDelayMs is the maximum delay between retries in milliseconds.
MaxRetryDelayMs *int
// ServerTools configures Anthropic server-side tools (web search, code execution, etc.).
// Only used when the model provider is Anthropic.
ServerTools []ai.AnthropicServerTool
// Compaction configures Anthropic server-side context compaction.
Compaction *ai.AnthropicCompaction
// OnPayload is an optional callback to inspect or replace provider payloads before sending.
// Return nil to keep the original payload unchanged.
OnPayload func(payload any, model *ai.Model) any
// OnRetry is invoked before a retryable pre-stream error (rate limit /
// overloaded / transient 5xx) is retried. Sessions can use this to notify
// the user that a retry is in flight.
OnRetry func(attempt int, delaySeconds float64, errMsg string)
}
AgentLoopConfig configures the agent loop.
type AgentMessage ¶
type AgentMessage struct {
ai.Message
// Custom holds extension-defined message types (e.g., BashExecutionMessage).
// When non-nil, the Message field may be empty and Custom determines the role.
Custom any `json:"custom,omitempty"`
}
AgentMessage is a message in the agent's conversation. It wraps an ai.Message and can be extended with custom message types.
func AgentLoop ¶
func AgentLoop( ctx context.Context, prompts []AgentMessage, agentCtx *AgentContext, config *AgentLoopConfig, streamFn StreamFn, events chan<- AgentEvent, ) []AgentMessage
AgentLoop starts an agent loop with new prompt messages. Events are emitted to the returned channel.
func AgentLoopContinue ¶
func AgentLoopContinue( ctx context.Context, agentCtx *AgentContext, config *AgentLoopConfig, streamFn StreamFn, events chan<- AgentEvent, ) ([]AgentMessage, error)
AgentLoopContinue continues an agent loop from the current context. Used for retries where context already has user message or tool results.
func NewAgentMessage ¶
func NewAgentMessage(msg ai.Message) AgentMessage
NewAgentMessage wraps an ai.Message as an AgentMessage.
func StripUnmatchedToolCalls ¶
func StripUnmatchedToolCalls(msgs []AgentMessage) []AgentMessage
StripUnmatchedToolCalls returns a copy of msgs with every assistant tool-call content block removed when no ToolResult message in msgs carries a matching ToolCallID. Assistant messages left with no content blocks are dropped entirely. All other messages pass through unchanged, and the input slice and its messages are never mutated.
This sanitizes a message snapshot for a one-shot side query. The snapshot is taken from live session state, which — because the assistant turn is committed on EventMessageEnd before its tools execute — can end with an in-flight tool call that has no result yet (notably the very `aside` invocation driving the side query). Appending a user question after such a dangling tool_use produces a malformed context: the model role-plays a continuation of the executor's turn (e.g. narrating that the tool "failed") instead of answering the question. Stripping the unmatched calls yields a well-formed context that ends on a complete turn.
func (*AgentMessage) Text ¶
func (m *AgentMessage) Text() string
Text returns the concatenated text of an assistant message's text content blocks (joined without separators, in source order), or "" if the message is not an assistant message or carries no text. It is the smallest viable way to pull the rendered answer out of an EventMessageEnd payload without walking AsAssistant().Content by hand.
type AgentOptions ¶
type AgentOptions struct {
// Model is the LLM model the agent runs on. Convenience field lifted
// from AgentState — see InitialState for the precedence rules.
Model *ai.Model
// SystemPrompt is the agent's system prompt. Convenience field lifted
// from AgentState — see InitialState for the precedence rules.
SystemPrompt string
// ThinkingLevel is the agent's reasoning level. Convenience field lifted
// from AgentState — see InitialState for the precedence rules.
ThinkingLevel ThinkingLevel
// Tools is the agent's tool set. Convenience field lifted from
// AgentState — see InitialState for the precedence rules.
Tools *ToolSet
// InitialState restores a full AgentState (bulk restore, e.g. replaying a
// snapshot). It is layered ON TOP of the convenience fields above, so any
// field set on InitialState wins over the matching convenience field.
// Leave it nil and use the convenience fields for the common case.
InitialState *AgentState
// ConvertToLLM converts AgentMessages to LLM Messages before each call.
// Defaults to DefaultConvertToLLM (filters to user/assistant/toolResult)
// when nil — callers only set this for exotic context shaping.
ConvertToLLM func(messages []AgentMessage) ([]ai.Message, error)
// TransformContext is applied before ConvertToLLM for context pruning etc.
TransformContext func(ctx context.Context, messages []AgentMessage) ([]AgentMessage, error)
// SteeringMode: "all" = send all steering messages at once, "one-at-a-time" = one per turn.
SteeringMode string
// FollowUpMode: "all" = send all follow-up messages at once, "one-at-a-time" = one per turn.
FollowUpMode string
// StreamFn is a custom stream function. When nil, the agent falls
// back to DefaultStreamFn.
StreamFn StreamFn
// SessionID is forwarded to LLM providers for session-based caching.
SessionID string
// GetAPIKey resolves an API key dynamically for each LLM call.
GetAPIKey func(provider string) (string, error)
// ThinkingBudgets sets custom token budgets for thinking levels.
ThinkingBudgets *ai.ThinkingBudgets
// Transport is the preferred transport for providers that support multiple transports.
Transport ai.Transport
// MaxRetryDelayMs caps how long to wait for server-requested retries.
MaxRetryDelayMs *int
// ServerTools configures Anthropic server-side tools (web search, code execution, etc.).
ServerTools []ai.AnthropicServerTool
// Compaction configures Anthropic server-side context compaction.
Compaction *ai.AnthropicCompaction
// OnPayload is an optional callback to inspect or replace provider payloads before sending.
// Return nil to keep the original payload unchanged.
OnPayload func(payload any, model *ai.Model) any
// OnRetry is invoked before a retryable pre-stream error is retried.
OnRetry func(attempt int, delaySeconds float64, errMsg string)
}
AgentOptions configures an Agent.
type AgentState ¶
type AgentState struct {
SystemPrompt string
Model *ai.Model
ThinkingLevel ThinkingLevel
Tools *ToolSet
Messages []AgentMessage
IsStreaming bool
StreamMessage *AgentMessage
PendingToolCalls map[string]bool
Error string
}
AgentState holds the current state of the agent.
type AgentTool ¶
type AgentTool struct {
ai.Tool
// Label is a human-readable label for UI display.
Label string
// DisplayHint tells the TUI how to format this tool's execution.
// Nil means use built-in formatting or the generic fallback.
DisplayHint *ToolDisplayHint
// Execute runs the tool. The context can be cancelled for abort.
Execute func(
ctx context.Context,
toolCallID string,
params map[string]any,
onUpdate AgentToolUpdateCallback,
) (AgentToolResult, error)
}
AgentTool extends ai.Tool with execution capability.
type AgentToolResult ¶
type AgentToolResult struct {
// Content blocks supporting text and images.
Content []ai.ToolResultContent
// Details for UI display or logging.
Details any
// Meta is small, structured metadata the LLM should see alongside the
// content (e.g. a content hash). Copied onto ToolResultMessage.Meta and
// rendered for the provider-bound message only — internal consumers
// that join content blocks never see it.
Meta map[string]string
// IsError signals that the tool result represents an error,
// even when Execute returns a nil error. Used by extension hooks
// to mark a modified result as an error.
IsError bool
// Terminate hints that the agent should stop after the current tool batch.
// Early termination only happens when every finalized tool result in the batch
// sets this to true.
Terminate bool
// StatusMessage is a transient progress label for the UI (e.g.
// "Calling Read..."). It is only meaningful on partial-update
// results and never persisted.
StatusMessage string
}
AgentToolResult is the result of executing a tool.
type AgentToolUpdateCallback ¶
type AgentToolUpdateCallback func(partialResult AgentToolResult)
AgentToolUpdateCallback is called during streaming tool execution.
type BlockSummary ¶
type BlockSummary struct {
Type string `json:"type"`
Len int `json:"len"`
SigLen int `json:"sig_len,omitempty"`
}
BlockSummary is a compact description of a single content block from an assistant message. It carries enough to diagnose "empty" / redacted responses (where a thinking block with sig_len>0 and len=0 is the smoking gun) without keeping the raw payload around.
func SummarizeBlocks ¶
func SummarizeBlocks(content []ai.AssistantContent) []BlockSummary
SummarizeBlocks produces a BlockSummary slice for the given content. Exported so session-layer code can attach the same summary to a SideQueryResult on success.
type PlanEntry ¶
type PlanEntry struct {
Content string `json:"content"`
Status PlanEntryStatus `json:"status"`
Priority PlanEntryPriority `json:"priority"`
}
PlanEntry represents a single entry in a plan.
type PlanEntryPriority ¶
type PlanEntryPriority string
PlanEntryPriority represents the priority of a plan entry.
const ( PlanEntryPriorityHigh PlanEntryPriority = "high" PlanEntryPriorityMedium PlanEntryPriority = "medium" PlanEntryPriorityLow PlanEntryPriority = "low" )
type PlanEntryStatus ¶
type PlanEntryStatus string
PlanEntryStatus represents the status of a plan entry.
const ( PlanEntryStatusPending PlanEntryStatus = "pending" PlanEntryStatusInProgress PlanEntryStatus = "in_progress" PlanEntryStatusCompleted PlanEntryStatus = "completed" )
type ShouldStopAfterTurnContext ¶
type ShouldStopAfterTurnContext struct {
// Message is the assistant message that completed the turn.
Message *ai.AssistantMessage
// ToolResults are the tool result messages passed to the preceding turn_end event.
ToolResults []ai.ToolResultMessage
// Context is the current agent context after the turn's assistant message
// and tool results have been appended.
Context AgentContext
// NewMessages are the messages that this loop invocation will return if it
// exits at this point. Prompt runs include the initial prompt messages;
// continuation runs do not include pre-existing context messages.
NewMessages []AgentMessage
}
ShouldStopAfterTurnContext is the context passed to AgentLoopConfig.ShouldStopAfterTurn.
type SimplePromptOptions ¶
type SimplePromptOptions struct {
// Model overrides the LLM model used for this call.
// When non-nil, the provider is implied by Model.Api and the appropriate
// API key is resolved via the agent's GetAPIKey for that provider.
Model *ai.Model
// Reasoning overrides the thinking/reasoning effort.
// Empty string ("") inherits the agent's current ThinkingLevel.
// Use ai.ThinkingOff explicitly to disable thinking for this call.
Reasoning ai.ThinkingLevel
}
SimplePromptOptions overrides per-call settings for SimplePrompt. All fields are optional; nil/empty values inherit the agent's current state.
Used to support "advisor" patterns where a side query is routed to a different (typically stronger) model than the executor agent is running on.
type StreamFn ¶
type StreamFn func(model *ai.Model, ctx ai.Context, options *ai.SimpleStreamOptions) *ai.AssistantMessageEventStream
StreamFn is the function that creates an LLM streaming call.
type ThinkingLevel ¶
type ThinkingLevel = ai.ThinkingLevel
ThinkingLevel is an alias for ai.ThinkingLevel so all packages use the same type.
func ClampThinkingLevel ¶
func ClampThinkingLevel(requested ThinkingLevel, available []ThinkingLevel) ThinkingLevel
ClampThinkingLevel returns the highest level in `available` that is at or below `requested` on the canonical ladder (max→xhigh→high→medium→low→minimal→off).
If `requested` is empty, it returns "" unchanged (callers treat this as "no opinion, leave the current level alone").
If `available` is empty, it falls back to ThinkingOff.
If `requested` is not on the canonical ladder, it is returned unchanged when present in `available`, otherwise ThinkingOff is returned.
Example ¶
ExampleClampThinkingLevel shows how a host clamps a requested reasoning level to whatever the underlying model supports. The canonical ladder is max → xhigh → high → medium → low → minimal → off.
package main
import (
"fmt"
"github.com/kfet/agent"
)
func main() {
// A model that supports up to "high".
available := []agent.ThinkingLevel{
agent.ThinkingOff, agent.ThinkingLow, agent.ThinkingMedium, agent.ThinkingHigh,
}
fmt.Println(agent.ClampThinkingLevel(agent.ThinkingMax, available))
fmt.Println(agent.ClampThinkingLevel(agent.ThinkingMedium, available))
fmt.Println(agent.ClampThinkingLevel("", available))
}
Output: high medium
type TitleArg ¶
type TitleArg struct {
// Name is the JSON parameter name.
Name string `json:"name"`
// Style controls how the value is rendered: "path" shortens and accents
// it, "pattern" wraps it in /…/, "accent" just colours it. Empty string
// means plain text.
Style string `json:"style,omitempty"`
// Label is an optional prefix shown before the value (e.g. "in").
Label string `json:"label,omitempty"`
}
TitleArg describes a single argument to display on the tool header line.
type ToolClassification ¶
type ToolClassification struct {
Builtin []string // sorted built-in tool names
Extensions map[string][]string // extension name → sorted tool names
}
ToolClassification holds tools grouped into built-in and per-extension buckets. MCP tools (prefixed "mcp__") are excluded.
type ToolDisplayHint ¶
type ToolDisplayHint struct {
// TitleArgs lists argument names to show on the header line, in order.
TitleArgs []TitleArg `json:"title_args,omitempty"`
// ResultMaxLines is the default number of result lines shown when
// collapsed. Zero means use the default (10).
ResultMaxLines int `json:"result_max_lines,omitempty"`
// UseBox renders the tool output in a bordered box (like bash).
UseBox bool `json:"use_box,omitempty"`
}
ToolDisplayHint tells the UI how to format a tool's execution display. Extensions provide this when registering tools so the TUI can render them nicely instead of falling back to a raw JSON dump.
type ToolSet ¶
type ToolSet struct {
// contains filtered or unexported fields
}
ToolSet is an ordered, name-unique collection of AgentTool values. Adding a tool with a name that already exists overwrites the previous entry (keeping insertion order of the *first* occurrence). This makes duplicate tool names structurally impossible.
All read methods are nil-safe: calling them on a nil *ToolSet returns zero values rather than panicking.
func ToolSetFrom ¶
ToolSetFrom builds a ToolSet from a slice of tools. If the slice contains duplicate names, the last entry wins.
func (*ToolSet) Add ¶
Add inserts or replaces a tool. If a tool with the same name already exists, the definition is updated in place without changing order.
func (*ToolSet) ClassifyTools ¶
func (ts *ToolSet) ClassifyTools(extensionTools map[string][]string) ToolClassification
ClassifyTools partitions the tool set into built-in and per-extension groups, excluding MCP tools. extensionTools maps extension name → tool names and is used to attribute tools to their owning extension; any non-MCP tool not in the map is considered built-in. Both the built-in list and per-extension lists are sorted alphabetically.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package tools provides the standard coding toolbox for the agent runtime: bash, read, write, edit, editdiff, find, grep, imageresize, and plan.
|
Package tools provides the standard coding toolbox for the agent runtime: bash, read, write, edit, editdiff, find, grep, imageresize, and plan. |