Documentation
¶
Overview ¶
Package agent implements the Octo agent core: messages, history, and the run loop that ties an LLM provider to user input.
The package is intentionally thin in M1.2 — it covers the "send a user message, receive an assistant reply" path with no streaming, no tool use, and no multi-turn cancellation. Those land in M2 / M3 alongside the provider streaming aggregators and the tool registry.
Index ¶
- Constants
- Variables
- func AttachmentNote(path string) string
- func AttachmentPaths(s string) []string
- func CacheUtilizationPct(inputTokens, cacheRead, cacheWrite int) (pct int, ok bool)
- func ContextWindow(model string) int
- func DeleteSession(id string) error
- func EstimateTextTokens(s string) int
- func EstimateTokens(msgs []Message) int
- func FirstUserSnippet(msgs []Message) string
- func FormatElapsedSeconds(seconds int64) string
- func FormatGoalElapsed(seconds int64) string
- func FormatGoalTokens(n int64) string
- func GoalStatusLabel(status GoalStatus) string
- func GoalUsageLine(g Goal) string
- func IsAutoNamePlaceholder(title string) bool
- func IsPlainUserMessage(m Message) bool
- func IsRateLimitErr(err error) bool
- func IsSettledAssistantMessage(m Message) bool
- func ResolveSessionID(input string) (string, error)
- func SessionMTime(id string) (time.Time, error)
- func SessionsDir() (string, error)
- func StripAttachmentNotes(s string) (cleaned string, names []string)
- func StripRemindersForDisplay(s string) string
- func StripSystemReminders(s string) string
- func Texts(items []InboxItem) []string
- func UserFacingError(err error) string
- func ValidateGoalObjective(objective string) error
- func WrapGoalContext(text string) string
- type Agent
- func (a *Agent) AccrueChildUsage(inputTokens, outputTokens, cacheRead, cacheWrite int)
- func (a *Agent) AttachUserBlocks(blocks []ContentBlock)
- func (a *Agent) AttachUserCreatedAt(t time.Time)
- func (a *Agent) ClearHistory()
- func (a *Agent) ConsolidateMemory(ctx context.Context, priorSummary, newNotes string) (string, error)
- func (a *Agent) ContextUsage() (used, window int)
- func (a *Agent) ForceCompact(ctx context.Context, handler EventHandler) (CompactStats, error)
- func (a *Agent) GenerateTitle(ctx context.Context) (string, error)
- func (a *Agent) GenerateTitleFrom(ctx context.Context, snap []Message) (string, error)
- func (a *Agent) GenerateTitleOrSnippet(ctx context.Context, snap []Message) (string, error)
- func (a *Agent) GetModel() string
- func (a *Agent) GetSender() Sender
- func (a *Agent) InputRolledBack() bool
- func (a *Agent) PersistContextUsage(sess *Session) error
- func (a *Agent) RealContextTokens() int
- func (a *Agent) ResetGoalBaseline()
- func (a *Agent) Run(ctx context.Context, userInput string, tools []ToolDefinition, ...) (reply Reply, err error)
- func (a *Agent) RunStream(ctx context.Context, userInput string, tools []ToolDefinition, ...) (reply Reply, err error)
- func (a *Agent) SessionCacheTokens() (readTokens, writeTokens int)
- func (a *Agent) SessionTokens() (inputTokens, outputTokens int)
- func (a *Agent) SetImageDescriber(d ImageDescriber)
- func (a *Agent) SetModel(model string)
- func (a *Agent) SetSender(s Sender)
- func (a *Agent) Suggest(ctx context.Context, tools []ToolDefinition) (string, error)
- func (a *Agent) TakeBackInterrupted() bool
- func (a *Agent) Turn(ctx context.Context, userInput string) (Reply, error)
- func (a *Agent) TurnIterations() int
- func (a *Agent) TurnStream(ctx context.Context, userInput string, onChunk func(textDelta string), ...) (Reply, error)
- type AgentEvent
- type BindResult
- type CompactStats
- type ContentBlock
- func NewImageBlock(mimeType string, data []byte) (ContentBlock, bool)
- func NewTextBlock(text string) ContentBlock
- func NewThinkingBlock(thinking, signature string) ContentBlock
- func NewToolResultBlock(toolUseID, result string, isError bool) ContentBlock
- func NewToolUseBlock(id, name string, input map[string]any) ContentBlock
- type EventHandler
- type EventKind
- type Goal
- type GoalAccountant
- type GoalCommandStart
- type GoalStatus
- type History
- func (h *History) Append(m Message)
- func (h *History) FindSystemMsg() int
- func (h *History) Len() int
- func (h *History) ReplaceAll(msgs []Message)
- func (h *History) Reset()
- func (h *History) RewriteDirty() bool
- func (h *History) Snapshot() []Message
- func (h *History) Tail(n int) []Message
- func (h *History) TruncateTo(n int)
- func (h *History) UpdateMessage(i int, mutate func(*Message))
- type ImageData
- type ImageDescriber
- type Inbox
- type InboxItem
- type LowEffortSender
- type Message
- type NoReasoningSender
- type PermissionGate
- type Reply
- type Role
- type Sender
- type Session
- func (s *Session) AccountGoalUsage(tokenDelta int64) (Goal, bool)
- func (s *Session) Bind(entry string, steal bool) (BindResult, string, error)
- func (s *Session) BoundTo(entry string) bool
- func (s *Session) ChunkDir() (string, error)
- func (s *Session) ClearComposedSystem() error
- func (s *Session) ClearGoal() bool
- func (s *Session) ClearLease() error
- func (s *Session) ConsumeGoalBudgetSteer() (string, bool)
- func (s *Session) ConsumeGoalObjectiveSteer() (string, bool)
- func (s *Session) CreateGoal(objective string, tokenBudget int64) (Goal, error)
- func (s *Session) DecFlight()
- func (s *Session) DisplayTitle() string
- func (s *Session) EditGoalObjective(objective string) (Goal, error)
- func (s *Session) EffectiveAgentID() string
- func (s *Session) EndsMidTurn() bool
- func (s *Session) FallbackTitleIfPlaceholder() string
- func (s *Session) GoalContinuation() (string, bool)
- func (s *Session) GoalContinuationPending() bool
- func (s *Session) GoalSnapshot() (Goal, bool)
- func (s *Session) IncFlight()
- func (s *Session) IsBound() bool
- func (s *Session) IsComposedFor(model, cwd, sourceDirs string) bool
- func (s *Session) LeaseActive() (string, bool)
- func (s *Session) MarkHookStarted()
- func (s *Session) ReplaceGoal(objective string, tokenBudget int64) (Goal, error)
- func (s *Session) ResetGoalWallClock()
- func (s *Session) Save() error
- func (s *Session) SavePath() (string, error)
- func (s *Session) SetAgentID(id string) error
- func (s *Session) SetBoundEntry(entry string, at time.Time)
- func (s *Session) SetComposedSystem(system, lean, model, cwd, sourceDirs string) error
- func (s *Session) SetGoalStatus(status GoalStatus) (Goal, error)
- func (s *Session) SetLastContextTokens(n int) error
- func (s *Session) SetModelConfig(name, model string) error
- func (s *Session) SetPermissionMode(mode string) error
- func (s *Session) SetTitle(title string) error
- func (s *Session) SetWorkingDir(dir string) error
- func (s *Session) ShortID() string
- func (s *Session) SuppressGoalContinuation()
- func (s *Session) SyncFrom(h *History)
- func (s *Session) ToHistory() *History
- func (s *Session) TruncateTo(n int)
- func (s *Session) TurnCount() int
- func (s *Session) Unbind(entry string) bool
- func (s *Session) UsedTools() bool
- func (s *Session) WriteLease(entry string, expires time.Time) error
- type StreamingSender
- type StreamingToolExecutor
- type ThinkingDeltaFunc
- type ToolDefinition
- type ToolExecutor
- type ToolInputDeltaFunc
- type ToolResult
- type ToolSender
- type ToolStreamingSender
Constants ¶
const ( StopReasonMaxTurns = "max_turns" StopReasonInterrupted = "interrupted" // StopReasonMaxTokens is the canonical output-truncation sentinel. Provider // adapters normalise their wire value to it (Anthropic "max_tokens", OpenAI // "length") so the loop checks one thing. The loop also reuses it as the // synthetic StopReason when a turn is ended because the response stayed // truncated even after escalation. See dev-docs/truncation-recovery.md. StopReasonMaxTokens = "max_tokens" // StopReasonStuck is set when the agentic loop detects consecutive duplicate // tool calls — a sign the model is stuck in a loop with no progress. The run // ends gracefully so the caller can intervene (e.g. prompt the user or retry // with a different strategy) rather than burning the full turn budget. StopReasonStuck = "stuck" )
StopReason sentinels set on the Reply when a loop budget is exhausted. They are NOT provider stop reasons — the agent synthesises them so callers can distinguish "the model finished" from "we cut it off".
const ( EntryCLI = "cli" EntryTUI = "tui" EntryWeb = "web" EntryAPI = "api" EntryChannel = "channel" EntryCron = "cron" EntrySetup = "setup" )
Common entry names. Use these constants at call sites so typos are caught.
const EventToolOutputCap = 8 * 1024
EventToolOutputCap is the maximum length of the Output field emitted on EventToolDone / EventToolError. The agent loop never truncates the actual tool result going back into the conversation — this cap only applies to what's surfaced to event observers (Web UI cards, IM previews), where a 100KB shell dump would be useless noise.
8KB is enough for web_fetch/read_file/grep previews (first ~40 lines) to remain useful while keeping event payloads small; the frontend folds long outputs anyway.
const GoalCommandUsage = "Usage: /goal <objective> · /goal edit <objective> · /goal pause|resume|clear · /goal replace <objective>"
GoalCommandUsage is the one-line grammar hint for the text-reply form of the command (the web slash command's notice).
const MaxGoalObjectiveChars = 4000
MaxGoalObjectiveChars caps the objective length (in runes, matching the Codex limit the feature is ported from).
const TitleGenerationTimeout = 5 * time.Second
TitleGenerationTimeout bounds a throwaway session-title call. The TUI and the server share one title mechanism: within this budget the model produces a title, otherwise GenerateTitleOrSnippet falls back to a message snippet, so a title always lands ~5s after the first user message.
const ToolResultMaxBytes = 40_000
ToolResultMaxBytes is the per-tool-result size backstop. A single tool result larger than this (a multi-MB file read, a grep with thousands of hits, a chatty build log) is truncated middle-out before it enters history, so one pathological call can't dominate the context window. It's a backstop, not a tuning knob: the value is generous enough that ordinary large outputs pass through untouched.
Truncation happens at production time (in dispatchTools), NOT by rewriting old history — so it never mutates already-sent messages and therefore never invalidates the conversation prompt cache. The user still sees the full output live via streaming tool-progress events; only the copy retained for the model is capped.
Variables ¶
var ErrSessionBoundToOther = fmt.Errorf("session is bound to another entry")
ErrSessionBoundToOther is returned when an entry tries to use a session owned by another entry without permission to steal.
Functions ¶
func AttachmentNote ¶ added in v1.10.6
AttachmentNote formats the note appended to a user message's text to tell the model where an uploaded file landed on disk (so it can read_file it). It is model-facing and persisted; display surfaces strip it with StripAttachmentNotes and render a chip from the filename instead.
func AttachmentPaths ¶ added in v1.12.13
AttachmentPaths returns the raw on-disk paths from every "[Attached file: <>]" note in s, in order. This is the complement of StripAttachmentNotes: callers that need the full path (e.g. to derive an /api/uploads/ thumbnail URL) use this; callers that only need a display name use StripAttachmentNotes.
func CacheUtilizationPct ¶ added in v1.15.15
CacheUtilizationPct returns the share of a turn's prompt tokens that were served from the provider's prompt cache: read / (input + read + write). InputTokens and CacheRead/WriteTokens are non-overlapping buckets (see accrueUsage), so the denominator is the whole prompt sent. ok is false when the backend reported no cache activity at all — callers omit the readout then instead of rendering a misleading "cache 0%". A warming turn (write only, read 0) does report 0%, which is honest: the cache exists but nothing was served from it yet.
func ContextWindow ¶
ContextWindow exposes contextWindow to other packages (e.g. the tools layer's Tool Search threshold) without duplicating the model→window table.
func DeleteSession ¶
DeleteSession permanently removes the transcript file for the given session id — session deletes bypass the trash, so there is nothing to recall. id may be a bare id or one with a .jsonl/.json suffix; absolute paths are rejected, and any id that would resolve outside the sessions directory (e.g. via "..") is refused so a caller-supplied id can't reach arbitrary files. A missing file is treated as success — deleting an already-gone session is not an error.
func EstimateTextTokens ¶ added in v1.15.18
EstimateTextTokens exposes estimateText for the same reason — the web server folds a resumed session's frozen system prompt into its cold-start context-percent estimate, which the transcript-only count omits.
func EstimateTokens ¶ added in v1.10.4
EstimateTokens exposes estimateMessages to other packages (e.g. the web server's cold-start context-percent estimate for a resumed session with no live Agent yet) so they share the exact same heuristic as compaction and ContextUsage instead of maintaining a second implementation.
func FirstUserSnippet ¶ added in v1.12.19
FirstUserSnippet extracts a one-line preview from the first user message, skipping injected <system-reminder> blocks and tool-result turns, and truncating to a list-friendly width.
func FormatElapsedSeconds ¶ added in v1.8.0
FormatElapsedSeconds renders whole seconds always down to the second: 45s, 12m30s. Unlike FormatGoalElapsed (which drops the remainder once minutes take over, since goal budgets run long), a single turn is short enough that dropping the seconds reads as suspiciously round — this mirrors the web frontend's fmtDur exactly so the per-turn summary line looks identical across the CLI, Web, and IM surfaces.
func FormatGoalElapsed ¶ added in v1.7.0
FormatGoalElapsed renders whole seconds compactly: 45s, 12m, 1h 30m, 2d 3h 5m.
func FormatGoalTokens ¶ added in v1.7.0
FormatGoalTokens renders a token count compactly: 950, 12.5K, 1.2M.
func GoalStatusLabel ¶ added in v1.7.0
func GoalStatusLabel(status GoalStatus) string
GoalStatusLabel is the human-readable status name shared by every surface.
func GoalUsageLine ¶ added in v1.7.0
GoalUsageLine summarizes a goal's spend: "12m, 63.9K/50K tokens".
func IsAutoNamePlaceholder ¶ added in v1.12.21
IsAutoNamePlaceholder reports whether a session title is absent or still an auto-assigned placeholder — "*Octo Agent" (agent.NewSession's default) or the frontend's "Session N" — both get replaced by a generated title after the first completed turn. A name the user typed themselves is kept. This is THE placeholder predicate: the generation gate, the turn-end adoption, and every list overlay must agree on it or a title is generated but never adopted (or vice versa).
func IsPlainUserMessage ¶ added in v1.14.8
IsPlainUserMessage reports whether m is a real user turn: role user with no tool_result blocks (tool results ride on synthetic user-role messages). History may only be cut just before such a message — anywhere else splits an assistant tool_use from the tool_result answering it. Compaction's split point and the server's branch endpoint both enforce this invariant.
func IsRateLimitErr ¶ added in v1.7.0
IsRateLimitErr classifies a turn error as a provider rate/quota limit. Provider adapters surface non-2xx responses as "<vendor>: HTTP <code>: ..." (the retry layer has already retried transient 429s by the time one reaches here), so a sustained limit is matched on the status code plus the common textual variants gateways use. Transports use it to park a failing goal-continuation turn as usage_limited.
func IsSettledAssistantMessage ¶ added in v1.16.1
IsSettledAssistantMessage reports whether m closes a turn: role assistant with no tool_use block still awaiting a result. A history prefix ending at such a message is a valid resume point — the same invariant IsPlainUserMessage expresses from the other side of the cut, and what the server's branch endpoint checks on the prefix it copies.
func ResolveSessionID ¶
ResolveSessionID maps a user-typed identifier to a full session ID. Accepted shapes:
- "last" → the most-recently-modified session
- the full session ID → returned as-is (fast path; never walks the dir)
- any substring of an ID → unique match required
On zero matches returns a "no session matches" error; on multiple, an ambiguity error listing the candidates so the user can re-disambiguate. The returned ID is suitable for passing to LoadSession.
func SessionMTime ¶
SessionMTime returns the file mtime of the session whose id is given. Used by the C9 Phase 2 memory daemon to gate "is this session quiet long enough to safely run boundary extraction" — the chat path updates the session file on every turn, so a recent mtime means the user is still actively chatting and the daemon should defer.
func SessionsDir ¶ added in v1.16.1
SessionsDir returns (and creates if needed) the directory transcripts live in. Exported for callers that need to observe the directory itself rather than load what is in it — the server's store watch counts entries there to notice sessions another process created, which loading them all would be a wasteful way to learn.
func StripAttachmentNotes ¶ added in v1.10.6
StripAttachmentNotes removes "[Attached file: <path>]" notes from display text and returns the cleaned text plus the display filename of each note (in order) so a UI can render an attachment chip. The name is the path basename with the upload timestamp prefix removed, so every surface (web bubble, reloaded transcript, TUI echo) shows the original filename. The notes stay in the persisted, model-facing content — only rendering paths call this. Text with no notes is returned byte-identical.
func StripRemindersForDisplay ¶
StripRemindersForDisplay removes <system-reminder> spans from a tool result before it reaches a UI surface (event stream, web history replay). The spans stay in the persisted blocks — the model must still read them — but a hook like the memory save-nudge must not render in tool cards. Text without reminders is returned byte-identical, so ordinary tool output is untouched.
func StripSystemReminders ¶
StripSystemReminders removes the runtime-injected model-facing spans from user text: <system-reminder> (background-process completion notes, recalled memories, …) and <goal_context> (goal continuation and steering prompts). Neither is user speech — strip them anywhere user text is rendered (session previews, the web transcript, steer bubbles) so they don't leak into the UI. A message that was pure injected context strips to empty, which every caller already treats as "render nothing".
func Texts ¶
Texts returns the text of each item, preserving order. Helper for callers that only need the string slice (e.g. background notifications).
func UserFacingError ¶ added in v1.12.21
UserFacingError strips internal agent-loop, dispatch, and provider prefixes from an error for display to end users. For example:
"agent: loop[0]: anthropic: HTTP 403 ..." → "HTTP 403 ..." "agent: dispatch tools[1]: openai: HTTP 429 ..." → "HTTP 429 ..."
func ValidateGoalObjective ¶ added in v1.6.1
ValidateGoalObjective enforces the objective contract shared by every mutation surface (slash commands, tools, HTTP API).
func WrapGoalContext ¶ added in v1.7.0
WrapGoalContext wraps a rendered steering prompt in the <goal_context> markers that hide it from UI surfaces while flagging its provenance to the model.
Types ¶
type Agent ¶
type Agent struct {
Sender Sender
System string
Model string
MaxTokens int
History *History
// LeanSystem, when set, is a lighter variant of System (skills manifest and
// memory dropped) used to seed cheap read-only sub-agents. Empty falls back
// to System.
LeanSystem string
// LiteSender/LiteModel, when both set, run cheap internal calls —
// history summarisation (compaction) and session-title generation — on a
// cheaper model. Unset falls back to Sender/Model. On a lite-call error
// summarize retries once on the primary sender, so a misconfigured lite
// model can't break compaction; title generation instead surfaces the
// error to GenerateTitleOrSnippet's snippet fallback (no retry).
LiteSender Sender
LiteModel string
// Describer, when non-nil, renders images as text for a primary model that
// can't accept image input. The pre-send transform consults it every turn
// (see describeImages).
//
// The agent knows nothing about vision: whether descriptions are needed at
// all, which endpoint answers, and what prompt it gets are all decided
// behind this interface (app.NewVisionDescriber builds it). Guarded by mu
// like Sender, since /model can swap the model underneath a running turn.
Describer ImageDescriber
// CWD is the working directory used to resolve project context (e.g.
// .octorules) for the planner. Callers should set this to the repo root
// before invoking PlanTask.
CWD string
// Gate, when non-nil, vets every tool call before execution. A nil
// Gate means no gating — all tool calls run (the pre-M6.5 behaviour).
Gate PermissionGate
// MaxTurns caps the number of provider round-trips in a single Run/
// RunStream. <= 0 uses defaultMaxTurns. Hitting the cap ends the run
// with a friendly budget reply (StopReason "max_turns"), not an error.
MaxTurns int
// MaxTokensEscalate is the per-response cap retried once, from unchanged
// history, when a round is truncated by the output cap (StopReasonMaxTokens).
// It only ever raises the cap: escalation fires only when this exceeds the
// round's current cap. <= 0 disables escalation. See
// dev-docs/truncation-recovery.md.
MaxTokensEscalate int
// CompactThreshold controls history compaction: when the most recent
// context sent (lastInputTokens) crosses the effective trigger, the next
// Run/RunStream summarizes the older turns before continuing. Semantics:
// <0 disables; ==0 auto (a fraction of the model's context window, the
// default); >0 is an explicit token count. See compactTriggerTokens.
CompactThreshold int
// CompactAutoFraction is the share (0.0–1.0) of the model's context window
// at which auto-compaction triggers when CompactThreshold == 0. Zero uses
// the built-in default (0.75). Values outside 0–1 are clamped.
CompactAutoFraction float64
// CompactKeepFraction is the share (0.0–1.0) of the model's context window
// that compaction keeps verbatim as the recent tail; everything older is
// folded into the summary. Zero uses the built-in default (0.30). It is
// always capped below the trigger (at half the trigger) so a compaction can
// reliably bring the context under the trigger with headroom to spare. See
// compactKeepBudget and dev-docs/compaction-redesign.md.
CompactKeepFraction float64
// ArchiveDir, when non-empty, is the directory into which compaction writes
// the verbatim originals of folded turns (chunk-NNN.md) before replacing
// them with the summary, so the model can recall details with the read
// tool. Set by the session-owning layer (CLI/server) via Session.ChunkDir;
// empty disables archival. Archival is best-effort — a write failure never
// breaks a compaction. See dev-docs/compaction-redesign.md.
ArchiveDir string
// Inbox holds user messages that arrived while a turn was running.
// The run loop drains it at the start of each iteration, before the LLM
// call, so messages enter history in chronological order. This mirrors
// Ruby octo's @inbox and keeps mid-turn input handling simple.
Inbox Inbox
// GoalAcct, when set, receives goal usage accounting after each LLM
// reply. Wired by the session-owning layer (Session implements
// GoalAccountant); nil disables goal accounting. The durable goal lives
// on the Session so per-turn Agents (serve rebuilds one each turn) all
// account into the same record.
GoalAcct GoalAccountant
// Hooks is the per-Agent hook engine. It supersedes the old single-slot
// UserInputHook/ToolResultHook: the memory injector registers its reminder
// (UserPromptSubmit) and save-nudge (PostToolUse) as in-process hooks on it,
// and any shell hooks (env or hooks.yml) live here too, so every transport
// runs one dispatch path. The engine shares a process-level seen-set so
// SessionStart resume fires once per OS process. Nil is a no-op.
Hooks *hooks.Engine
// HookMeta carries the session identity (id, transport, transcript, cwd,
// model) folded into every hook Payload. Set by the session-owning layer
// before a run, alongside ArchiveDir. Model falls back to a.Model when
// unset.
HookMeta hooks.Meta
// SessionStarted mirrors the session's durable "SessionStart has fired"
// flag, seeded by the session-owning layer before the run. The engine's
// SessionStartDecision uses it (with the process seen-set) to pick
// startup vs resume; the agent flips it and calls OnSessionStart when
// startup fires so the layer can persist it.
SessionStarted bool
// HookClear, when true, makes the next turn's SessionStart fire with
// source=clear (set by the session layer right after a /clear). Consumed
// once, on the next appended user turn.
HookClear bool
// OnSessionStart, if set, is invoked when SessionStart fires with
// source=startup — the seam the session layer uses to persist the durable
// flag (Session.MarkHookStarted). Runs on the turn goroutine.
OnSessionStart func()
// TurnEndReminder, when set, is consulted at the moment a turn would end:
// the model answered in prose and nothing else is pending. A non-empty
// return is appended as a user message and the loop runs one more round, so
// the model can act on it before the turn really closes; "" ends the turn.
// toolsUsed carries the tool names dispatched so far this turn, so a
// reminder can scope itself to turns that actually touched what it guards
// instead of re-billing a round-trip on every turn of the session.
//
// It is the seam for harness-side end-of-turn bookkeeping the model tends
// to forget — today the task-checklist guard (tools.PendingTaskReminder),
// which catches a plan left with an in_progress task after the model has
// already reported the work done. The agent layer stays ignorant of what
// is being checked; the ctx is the running turn's, so a ctx-scoped store
// (server / IM) resolves the same way the tools do.
//
// Fired at most once per turn: a model that ignores the reminder must not
// be able to hold the turn open. Wired only on top-level agents — a
// sub-agent shares the parent's checklist and must not report on it.
TurnEndReminder func(ctx context.Context, toolsUsed []string) string
// contains filtered or unexported fields
}
Agent owns one conversation: the system prompt, the history of turns, the model name, and the LLM transport (Sender).
func New ¶
New constructs an Agent with a fresh History.
Required: sender (otherwise Turn returns an error), model (otherwise the provider rejects the request). System and MaxTokens are optional.
func (*Agent) AccrueChildUsage ¶
AccrueChildUsage folds tokens spent by a spawned sub-agent into this agent's session totals, so SessionTokens and SessionCacheTokens still report one consolidated number even when the LLM used sub_agent. The child's cache read/write deltas ride along so the per-turn cache utilization readout (CacheUtilizationPct) stays a true value rather than a lower bound on turns that spawned sub-agents.
func (*Agent) AttachUserBlocks ¶
func (a *Agent) AttachUserBlocks(blocks []ContentBlock)
AttachUserBlocks queues content blocks — typically image blocks — to be folded into the next user message appended by a Turn/Run/RunStream call. The blocks are consumed exactly once (by the next appendUserInput) and then cleared. Call it immediately before the run so the text and the attachments land on the same user turn. Passing nil clears any queued blocks.
func (*Agent) AttachUserCreatedAt ¶
AttachUserCreatedAt pins the timestamp the next appended user message will carry, so a caller that pre-stamped the same message (the web server, which broadcasts a live created_at before the turn) gets an identical persisted timestamp rather than a second, later time.Now(). Consumed once by the next appendUserInput. Mirrors AttachUserBlocks.
func (*Agent) ClearHistory ¶
func (a *Agent) ClearHistory()
ClearHistory drops the entire conversation history, returning the agent to a fresh state while keeping its system prompt, model, and tool wiring intact. The context-usage gauge is reset; cumulative session token totals (cost accounting) are deliberately left alone. Backs the /clear command.
func (*Agent) ConsolidateMemory ¶
func (a *Agent) ConsolidateMemory(ctx context.Context, priorSummary, newNotes string) (string, error)
ConsolidateMemory runs the (incremental) consolidation side-call: it folds newNotes into priorSummary and returns the updated summary. Either argument may be empty — empty priorSummary means "first pass"; empty newNotes means "no new material" and the call short-circuits.
func (*Agent) ContextUsage ¶
ContextUsage reports how full the model's context window is: used is the most recently sent context size in tokens (reported by the provider), or, before any turn has run in this process (e.g. right after resuming a session), a heuristic estimate over the restored history — see historyTokens. window is the model's approximate context-window size. Lets the TUI status bar and the web UI render a "ctx N%" gauge. window is always > 0.
func (*Agent) ForceCompact ¶
func (a *Agent) ForceCompact(ctx context.Context, handler EventHandler) (CompactStats, error)
ForceCompact compacts the conversation now, regardless of the auto-trigger threshold — it backs the explicit /compact command. Like maybeCompact it reclaims stale tool results first (cheap, no LLM call) and then summarizes the oldest complete turns, but it does not gate on the context being "full" nor skip a small fold (anti-thrash): the user asked for it. It still no-ops when there aren't enough complete turns to fold safely. The returned stats report what changed (BeforeTokens == AfterTokens and FoldedMsgs == 0 means nothing was compacted).
func (*Agent) GenerateTitle ¶
GenerateTitle produces a short title for the conversation so far, for display in the session list. It is a throwaway provider call: the request carries only the first user message plus the instruction (never the live History, no system prompt, no tools), runs on the lite model when one is configured, and its token usage is not accrued into the session. Returns "" (no error) when there's no user text to title.
func (*Agent) GenerateTitleFrom ¶ added in v1.12.19
GenerateTitleFrom is GenerateTitle over an explicit message snapshot. It exists for title-on-receipt callers: when a turn starts, the loop goroutine owns History and hasn't appended the incoming user message yet, so the caller passes its own pre-turn snapshot plus that message instead.
The call runs on LiteSender/LiteModel when set, otherwise on the primary sender, and there is NO retry on the primary after a lite failure — the GenerateTitleOrSnippet snippet fallback already guarantees a title, and a retry would double the latency of a call bounded by TitleGenerationTimeout.
func (*Agent) GenerateTitleOrSnippet ¶ added in v1.12.19
GenerateTitleOrSnippet is GenerateTitleFrom with a guaranteed result: on error, timeout, or an empty model reply it falls back to a truncated snippet of the first user message in snap. This is THE session-title mechanism — the TUI and the server both call it (wrapped in TitleGenerationTimeout) so every frontend gets the same behaviour: an LLM title when the call works, a snippet otherwise, always within ~5s of the first user message. Returns "" only when snap carries no user text at all.
func (*Agent) GetModel ¶ added in v1.15.10
GetModel returns the agent's current model under a read lock. Pairs with SetModel for callers outside the turn goroutine (the image describer reads it mid-turn to decide whether descriptions are needed at all).
func (*Agent) GetSender ¶ added in v1.12.7
GetSender returns the agent's current sender under a read lock. Callers that need a consistent sender across multiple calls (e.g. type-asserting to StreamingSender AND calling SendMessages) should capture the returned value once and use that snapshot.
func (*Agent) InputRolledBack ¶ added in v1.14.7
InputRolledBack reports whether the most recent turn undid its own user input (the first-round-failure contract). True means the message the user sent is gone from History, so a UI that cleared its input box on send is the last place that text could come back from. False covers both a clean turn and a failure past the first round, where the message stayed in History.
func (*Agent) PersistContextUsage ¶ added in v1.12.2
PersistContextUsage records this agent's current context-window token count on the session (Session.LastContextTokens) so an idle or resumed session — one with no live Agent in memory — reports its true context usage instead of a transcript estimate that omits the system-prompt/tools overhead. Every transport (web, IM, CLI, scheduled) calls it at turn end, so a session opened in the Web UI shows the right number regardless of where it last ran. No-op when no count is available yet; best-effort — callers log any save error.
func (*Agent) RealContextTokens ¶ added in v1.15.18
RealContextTokens returns the provider-reported size of the most recently sent context, or 0 when no round-trip has reported usage yet (a fresh Agent before its first reply lands). Unlike ContextUsage it never falls back to the transcript estimate — for callers that have a better zero fallback (e.g. the persisted Session.LastContextTokens).
func (*Agent) ResetGoalBaseline ¶ added in v1.6.1
func (a *Agent) ResetGoalBaseline()
ResetGoalBaseline pins the goal-accounting baseline to the current session counters and restarts the goal wall clock, so the next accounting bills only usage — tokens and seconds — from this point on. Called at every turn start. (A goal created mid-turn is protected by the Session-side skip-next-delta flag instead, since the tool executor never sees the Agent.)
func (*Agent) Run ¶
func (a *Agent) Run(ctx context.Context, userInput string, tools []ToolDefinition, executor ToolExecutor) (reply Reply, err error)
Run is the agentic loop: it appends the user message to history then repeatedly calls the provider until the model reaches end_turn (no more tool calls) or the iteration cap is hit. Run is the buffered, no-event counterpart of RunStream — both drive the same runLoop, Run with a nil handler so no AgentEvents are emitted.
If tools is nil or executor is nil, Run is equivalent to Turn (single-turn, no tool dispatch).
func (*Agent) RunStream ¶
func (a *Agent) RunStream( ctx context.Context, userInput string, tools []ToolDefinition, executor ToolExecutor, handler EventHandler, ) (reply Reply, err error)
RunStream is the streaming agentic loop. Behaves like Run but emits structured AgentEvents to handler as work progresses — text deltas, tool start/done/error, and a final EventTurnDone carrying the aggregated Reply.
If tools is nil or executor is nil, RunStream falls back to TurnStream and adapts text deltas into EventTextDelta events. handler may be nil, in which case events are discarded but the run completes normally.
func (*Agent) SessionCacheTokens ¶
SessionCacheTokens returns the cumulative cache read/write token counts. Read is input served from cache (cheap); write is input written into the cache (Anthropic only). Both zero when the backend reports no cache info.
func (*Agent) SessionTokens ¶
SessionTokens returns the cumulative input and output token counts for all turns made so far in this Agent's lifetime.
func (*Agent) SetImageDescriber ¶ added in v1.15.10
func (a *Agent) SetImageDescriber(d ImageDescriber)
SetImageDescriber installs (or clears, with nil) the image describer under a write lock. Called once during agent construction by the entry point that has the config in hand; nil leaves images untouched.
func (*Agent) SetModel ¶ added in v1.15.10
SetModel swaps the agent's model under a write lock. Callers that also swap the sender should use both setters — they guard the same mutex.
func (*Agent) SetSender ¶ added in v1.12.7
SetSender swaps the agent's sender under a write lock. Used by the TUI's /model and /thinking commands to rebuild the sender when the provider or base URL changes.
func (*Agent) Suggest ¶
Suggest produces a single follow-up message the user might want to send next, based on the conversation so far. It is a throwaway provider call: the instruction is appended to a snapshot of history (never to the live History), so it doesn't pollute the conversation, and its token usage is not accrued into the session. Returns "" (no error) when there's nothing to suggest.
tools should be the SAME toolbelt the agentic loop uses. Anthropic's cache prefix is ordered tools → system → messages, so sending the identical tools makes this call reuse the main conversation's prompt cache (the whole history is billed at the cheap cache-read rate) instead of re-billing it in full. Without tools the prefix diverges at block 0 and nothing is cached. The model is told not to call tools; if it returns a tool_use anyway, Content is empty and we simply produce no suggestion that turn.
func (*Agent) TakeBackInterrupted ¶ added in v1.12.19
TakeBackInterrupted undoes an interrupt that produced no output: when history ends with the assistant interrupt note sitting directly on a plain user message (no tool_results — nothing ran), both are removed and true is returned. UIs that recall the interrupted input into their compose box for editing (the TUI's Esc take-back) call this after the turn winds down, so the recalled text doesn't also linger in context as a ghost message. Any other tail shape means the turn made observable progress; it is left untouched and false is returned.
func (*Agent) Turn ¶
Turn appends the user's input to history, asks the Sender for a reply, appends the reply to history, and returns it. Errors leave History unchanged from before the call.
func (*Agent) TurnIterations ¶
TurnIterations returns the number of provider round-trips executed during the most recent Run/RunStream call. It is 0 before the first run.
func (*Agent) TurnStream ¶
func (a *Agent) TurnStream( ctx context.Context, userInput string, onChunk func(textDelta string), onThinking func(thinkingDelta string), ) (Reply, error)
TurnStream is the streaming counterpart of Turn. It appends the user input to history, calls the Sender (streaming if supported, otherwise falling back to SendMessages), invokes onChunk for each text delta, appends the final assistant reply to history, and returns it.
onChunk may be nil, in which case the stream is still consumed end-to-end but no per-delta callback fires — useful for tests and for callers that only want the aggregated Reply.
On error, the user message is popped from History (same contract as Turn), so a retry with the same History doesn't duplicate the user turn.
type AgentEvent ¶
type AgentEvent struct {
Kind EventKind `json:"kind"`
Text string `json:"text,omitempty"`
ToolID string `json:"tool_id,omitempty"`
ToolName string `json:"tool_name,omitempty"`
Input map[string]any `json:"input,omitempty"`
InputDelta string `json:"input_delta,omitempty"`
Chunk string `json:"chunk,omitempty"`
Output string `json:"output,omitempty"`
Err string `json:"err,omitempty"`
// UI is the tool's optional structured result payload (EventToolDone).
// Unlike Output it is never truncated — it is already a compact summary
// built by the tool itself (see ToolResult.UI).
UI any `json:"ui,omitempty"`
Reply *Reply `json:"reply,omitempty"`
Messages []string `json:"messages,omitempty"`
Compact *CompactStats `json:"compact,omitempty"`
Goal *Goal `json:"goal,omitempty"`
// Steer carries the full inbox items behind an EventSteerInjected —
// including attachment blocks — for handlers that render more than the
// plain texts in Messages.
Steer []InboxItem `json:"-"`
// Image* describe one image going through the vision helper
// (EventImageDescribing). ImageName is the file's basename, or "image"
// when the block has no on-disk copy; ImageIndex/ImageTotal are 1-based
// over the images needing description this turn.
ImageName string `json:"image_name,omitempty"`
ImageIndex int `json:"image_index,omitempty"`
ImageTotal int `json:"image_total,omitempty"`
ImageStatus string `json:"image_status,omitempty"`
// SteerBaseIndex is the history position the first steer item was appended
// at (EventSteerInjected only). Item k of Steer/Messages lands at
// SteerBaseIndex+k, so a persistence-aware handler can label each steered
// user message with its true message_index for later edit/branch.
SteerBaseIndex int `json:"-"`
}
AgentEvent is the union shape carried over the EventHandler callback.
All fields are populated only for the EventKinds that need them; the rest stay at zero values. The contract for each kind:
- EventTextDelta: Text
- EventThinkingDelta: Text
- EventToolInputDelta: ToolID, ToolName, InputDelta
- EventToolStarted: ToolID, ToolName, Input
- EventToolProgress: ToolID, ToolName, Chunk
- EventToolDone: ToolID, ToolName, Output, UI (when the tool provides one)
- EventToolError: ToolID, ToolName, Output (may be empty), Err
- EventTurnDone: Reply
- EventSteerInjected: Messages
- EventCompactStarted: Compact (BeforeTokens, FoldedMsgs, KeptTurns, MaxTokens)
- EventCompactProgress: Chunk, Compact (SummaryTokens, MaxTokens)
- EventCompactDone: Compact (BeforeTokens, AfterTokens, FoldedMsgs)
- EventGoalUpdated: Goal
- EventImageDescribing: ImageName, ImageIndex, ImageTotal, ImageStatus, Err (on "failed")
JSON tags are included so the WS transport (M8 web server) can marshal events directly without an intermediate type.
type BindResult ¶
type BindResult int
BindResult reports what happened in a Bind call.
const ( // Bound indicates the session is now bound to the caller. Bound BindResult = iota // AlreadyBound indicates the caller already owns the binding. AlreadyBound // Stolen indicates the binding was taken from another entry. Stolen // Rejected indicates another entry owns the session and steal was false. Rejected )
type CompactStats ¶
type CompactStats struct {
// BeforeTokens is the context size before compaction (real when available).
BeforeTokens int `json:"before_tokens,omitempty"`
// AfterTokens is the context size after compaction (real when available, done only).
AfterTokens int `json:"after_tokens,omitempty"`
// FoldedMsgs is how many leading messages were folded into the summary.
FoldedMsgs int `json:"folded_msgs,omitempty"`
// KeptTurns is how many recent user turns were kept verbatim.
KeptTurns int `json:"kept_turns,omitempty"`
// SummaryTokens is the running estimate of the summary generated so far
// (progress only).
SummaryTokens int `json:"summary_tokens,omitempty"`
// ReclaimedTokens is how many tokens the no-LLM stale-tool-result
// reclamation pass freed (done only). When set with FoldedMsgs == 0 the
// compaction was handled entirely by the cheap reclamation tier — no
// summarize call was made.
ReclaimedTokens int `json:"reclaimed_tokens,omitempty"`
// MaxTokens is the summary's output-token cap, for a "N / max" readout.
MaxTokens int `json:"max_tokens,omitempty"`
}
CompactStats carries the numbers behind the compaction events. BeforeTokens and AfterTokens prefer the provider's real input token count when available (lastInputTokens) and fall back to a heuristic estimate otherwise. They exist for a human-readable progress indicator, nothing more.
type ContentBlock ¶
type ContentBlock struct {
// Type distinguishes the block variant: "text", "tool_use", "tool_result",
// "thinking", "image".
Type string `json:"type"`
// Text is the text payload (type=="text").
Text string `json:"text,omitempty"`
// ID is the unique call identifier supplied by the model (type=="tool_use").
// ToolUseID on a tool_result block must match the ID of the corresponding
// tool_use block.
ID string `json:"id,omitempty"`
// Name is the tool name the model wants to invoke (type=="tool_use").
Name string `json:"name,omitempty"`
// Input is the parsed argument map the model passes to the tool
// (type=="tool_use"). Keys and value types are defined by the tool's
// JSON Schema Parameters.
Input map[string]any `json:"input,omitempty"`
// ToolUseID links this result back to its originating tool_use block
// (type=="tool_result"). Must equal the ID field of the paired block.
ToolUseID string `json:"tool_use_id,omitempty"`
// Result is the textual output of the tool execution (type=="tool_result").
Result string `json:"result,omitempty"`
// IsError signals that the tool execution failed (type=="tool_result").
// The LLM can inspect Result for the error message and recover gracefully.
IsError bool `json:"is_error,omitempty"`
// UI is an optional structured rendering of the result for UI consumers
// (type=="tool_result"). Persisted with the session so history replay can
// render rich result cards. Provider adapters build their wire payloads
// field-by-field and never serialise this — it is invisible to the model.
UI any `json:"ui,omitempty"`
// Thinking is the reasoning trace text (type=="thinking"). Anthropic-protocol
// reasoning models (Claude, Kimi k2.6) return it as a first-class content
// block that must be preserved and replayed on subsequent requests when tool
// use is in play, or the API rejects the follow-up.
Thinking string `json:"thinking,omitempty"`
// Signature authenticates a thinking block (type=="thinking"). It must be
// sent back verbatim alongside the thinking text on the next request.
Signature string `json:"signature,omitempty"`
// Reasoning carries an OpenAI-protocol thinking model's reasoning trace that
// must be echoed back on the next request (type=="tool_use"). deepseek-v4
// returns reasoning_content alongside a tool call and rejects the follow-up
// unless it's resent; the OpenAI adapter stashes it here so it round-trips
// through history. Providers that don't need it ignore the field.
Reasoning string `json:"reasoning,omitempty"`
// Image carries vision-model image data (type=="image"). Used when a tool
// result includes an image that should be rendered by a multimodal model
// (Claude, GPT-4o, Kimi k2.6, etc.). The provider adapter serialises it to
// the vendor-specific wire format (Anthropic base64 source, OpenAI data URL).
Image *ImageData `json:"-"`
// ImagePath points at a persisted on-disk copy of Image (type=="image").
// Image bytes are never serialised into the session transcript; a block
// saved with a path is rehydrated from it by LoadSession so a resumed
// conversation can re-send the image to the provider.
ImagePath string `json:"image_path,omitempty"`
// ImageDescription is the vision helper's rendering of this image as text
// (type=="image"), for primary models that can't accept image input. It is
// filled lazily by the pre-send transform (see describeImages) and persists
// with the session; non-empty means "already described", and the helper is
// never called for this block again — the block is its own cache.
//
// Only a successful description is stored. The failure fallback text goes
// into the outgoing snapshot alone, so this field never holds an apology.
ImageDescription string `json:"image_description,omitempty"`
// ImageDescFailures counts consecutive description failures for this block
// (type=="image"). At visionHelperMaxFailures the block stops calling the
// helper for the rest of the session, so a dead endpoint costs one timeout
// per image rather than one per turn. LoadSession resets it to zero, which
// is what gives a resumed session a fresh budget once the endpoint is
// fixed; it is serialised only so an in-process Save/Load round-trip
// doesn't lose the count mid-session.
ImageDescFailures int `json:"image_desc_failures,omitempty"`
}
ContentBlock is a single element of a multi-part message. It unifies the roles a block can play in an LLM conversation:
- "text" — plain assistant or user text
- "tool_use" — the model requesting a tool call (assistant turn)
- "tool_result" — the result of a tool call (user turn)
- "thinking" — a reasoning model's extended-thinking trace (assistant turn)
- "image" — an image for multimodal model consumption (user turn)
The zero value is not valid; use the New*Block helpers instead.
func NewImageBlock ¶
func NewImageBlock(mimeType string, data []byte) (ContentBlock, bool)
NewImageBlock creates a ContentBlock with Type=="image" for multimodal model consumption. The provider adapter is responsible for converting this to the vendor-specific wire format. The bytes are normalized on the way in (see compressImageData): oversized captures are downscaled and re-encoded so every attach path — clipboard, composer, IM, tool results — stays under provider size limits without each caller re-implementing it.
ok is false when the bytes aren't an image format the providers accept, in which case there is no block to send and the caller should describe the content in text instead. Sending one anyway costs the whole turn: the media type travels verbatim into Anthropic's `source.media_type` and OpenAI's data URL, and an unsupported value fails the entire request, not just the image.
The format is decided by sniffing the bytes, not by the caller's mimeType. Callers get that string from somewhere untrustworthy — a file extension, or an MCP server that names whatever it likes (including nothing) — and a wrong label is indistinguishable from a wrong image until the provider rejects it.
func NewTextBlock ¶
func NewTextBlock(text string) ContentBlock
NewTextBlock creates a ContentBlock with Type=="text".
func NewThinkingBlock ¶
func NewThinkingBlock(thinking, signature string) ContentBlock
NewThinkingBlock creates a ContentBlock with Type=="thinking". The signature authenticates the trace and must be preserved for the round-trip.
func NewToolResultBlock ¶
func NewToolResultBlock(toolUseID, result string, isError bool) ContentBlock
NewToolResultBlock creates a ContentBlock with Type=="tool_result". toolUseID must match the ID of the corresponding tool_use block. isError should be true when the tool execution failed; result carries the error message in that case.
func NewToolUseBlock ¶
func NewToolUseBlock(id, name string, input map[string]any) ContentBlock
NewToolUseBlock creates a ContentBlock with Type=="tool_use". id must be unique within the conversation turn (supplied by the LLM).
type EventHandler ¶
type EventHandler func(AgentEvent)
EventHandler is the callback type passed into Agent.RunStream. The handler is invoked synchronously from the agent loop — if it blocks, the loop blocks. Implementations that need async fan-out (e.g. WS to multiple clients) should buffer into a channel and return immediately.
type EventKind ¶
type EventKind string
EventKind tags an AgentEvent by what happened.
const ( // EventTextDelta carries one piece of the assistant's text reply, as it // arrives off the provider stream. Multiple deltas concatenate to form // the full reply text. EventTextDelta EventKind = "text_delta" // EventThinkingDelta carries one fragment of a reasoning model's thinking // trace, streamed before the visible reply. Text holds the fragment; // fragments concatenate to form the full trace. Emitted only when the Sender // surfaces reasoning (e.g. reasoning display is enabled); observers render it // dimmed and distinct from the answer. It is NOT part of Reply.Content. EventThinkingDelta EventKind = "thinking_delta" // EventToolInputDelta fires zero or more times while the LLM is // streaming a tool_use block's input arguments (e.g. write_file's // content field). ToolID / ToolName identify the call; InputDelta is // the raw JSON fragment as it arrived on the wire — fragments // concatenate to form the final JSON object. EventToolStarted (with // the fully-parsed Input map) still fires after the arguments are // complete and parsed. // // These events are useful for live-rendering large tool arguments // (e.g. showing a file's content as it's being written) in a Web UI. // Most CLI consumers can ignore them. EventToolInputDelta EventKind = "tool_input_delta" // EventToolStarted fires immediately before a tool is dispatched. // ToolID / ToolName / Input identify the call. EventToolStarted EventKind = "tool_started" // EventToolProgress fires zero or more times between EventToolStarted and // EventToolDone, surfacing incremental tool output (e.g. a long shell // command's stdout line-by-line). Only tools that implement // StreamingToolExecutor emit these; tools that don't are silent until // EventToolDone. Chunk carries the new fragment, NOT the running total — // it's not truncated (the consumer is responsible for any rate limiting // or batching). EventToolProgress EventKind = "tool_progress" // EventToolDone fires after a successful tool execution. // Output carries the tool's combined stdout/stderr text (truncated to // EventToolOutputCap if longer). EventToolDone EventKind = "tool_done" // EventToolError fires when the tool executor reports an error result // (IsError=true on the underlying ToolResultBlock). Err carries the // failure message; Output may still contain partial stdout from the // failing process. EventToolError EventKind = "tool_error" // EventTurnDone fires once at the end of a successful Run/RunStream, // after the assistant's final reply is committed to history. Reply // carries the aggregated final Reply. EventTurnDone EventKind = "turn_done" // EventTurnError marks a turn-level failure — the LLM call (or turn setup) // errored out, as opposed to a single tool failing (EventToolError). The // agent loop does not emit it; Run/RunStream returns the error directly and // transports surface it under this kind so consumers can tell a turn abort // from a per-tool error. Err carries the failure message. EventTurnError EventKind = "turn_error" // EventSteerInjected fires when the agent loop drains the inbox and // injects mid-turn user messages into history. Messages carries the // drained texts so observers (e.g. the TUI) can render them in the // transcript at the correct chronological position — before the next // assistant reply, not after the turn ends. EventSteerInjected EventKind = "steer_injected" // EventCompactStarted fires when history compaction begins, just before // the summarization side-call. Compact carries the pre-compaction context // estimate and how much is being folded so observers can show a "compacting // conversation history" indicator. Compaction is silent to the model; these // events exist only to keep the user informed. EventCompactStarted EventKind = "compact_started" // EventCompactProgress fires repeatedly while the summary streams back from // the model. Chunk carries the newest text fragment of the summary; // Compact.SummaryTokens is the running estimate of summary length so far. // Observers can show a live "generated ~N tokens" indicator. Fires only // when the underlying Sender streams; otherwise compaction jumps straight // from started to done. EventCompactProgress EventKind = "compact_progress" // EventCompactDone fires once compaction finishes (or fails). Compact // carries the before/after context estimates; when they are equal the // compaction was a no-op (summarization failed or returned nothing) and the // full history was kept. Observers should clear any compaction indicator. EventCompactDone EventKind = "compact_done" // EventGoalUpdated fires when the session goal record changes during a // turn — usage accounting moved the counters or a budget crossing flipped // the status. Goal carries the updated snapshot. Mutations made outside a // turn (slash commands, HTTP API) don't flow through here; the mutating // surface returns the Goal to its caller directly. EventGoalUpdated EventKind = "goal_updated" // EventImageDescribing fires around each image the pre-send transform hands // to the vision helper, so the UI can explain the pause before the model // starts replying. ImageStatus is "started", "done" or "failed"; // ImageName/ImageIndex/ImageTotal identify which image, and Err carries the // reason on "failed". Cached descriptions emit nothing — there is no wait // to explain. EventImageDescribing EventKind = "image_describing" )
type Goal ¶ added in v1.6.1
type Goal struct {
// ID identifies this goal instance. A replaced goal gets a new ID, which
// is what lets an in-flight continuation detect that the goal it queued
// for no longer exists.
ID string `json:"id"`
Objective string `json:"objective"`
Status GoalStatus `json:"status"`
// TokenBudget is the optional spend ceiling; 0 means unbudgeted.
TokenBudget int64 `json:"token_budget,omitempty"`
// TokensUsed accumulates non-cached input + output tokens while the goal
// was active (cache reads are deliberately free).
TokensUsed int64 `json:"tokens_used"`
// TimeUsedSeconds accumulates wall-clock seconds while the goal was active.
TimeUsedSeconds int64 `json:"time_used_seconds"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Goal is a session's persistent objective: the agent keeps pursuing it across turns until the status machine stops the continuation loop. At most one goal exists per session; replacing it mints a new ID with fresh usage counters.
func (*Goal) RemainingTokens ¶ added in v1.6.1
RemainingTokens reports the unspent budget, or -1 when unbudgeted.
type GoalAccountant ¶ added in v1.6.1
type GoalAccountant interface {
// AccountGoalUsage folds a token delta (non-cached input + output) and
// the elapsed wall-clock time into the goal, returning the updated goal
// and whether the record changed.
AccountGoalUsage(tokenDelta int64) (Goal, bool)
// ResetGoalWallClock re-baselines the wall clock at turn start, dropping
// the idle gap since the previous turn — idle time is not goal work.
ResetGoalWallClock()
// ConsumeGoalBudgetSteer returns the one-time budget-limit steering
// prompt when the last accounting crossed the token budget, for the agent
// loop to inject as a hidden steer.
ConsumeGoalBudgetSteer() (string, bool)
// ConsumeGoalObjectiveSteer returns the one-time steer staged when the
// objective was edited while a goal existed, for the agent loop to inject
// as a hidden steer so an in-flight turn adjusts to the new objective
// instead of finishing out the stale one.
ConsumeGoalObjectiveSteer() (string, bool)
}
GoalAccountant receives goal usage accounting from the agent loop after each LLM reply. Implemented by Session; the session-owning layer wires it into Agent.GoalAcct so per-turn Agents (serve rebuilds one per turn) all account into the same durable record.
type GoalCommandStart ¶ added in v1.15.12
type GoalCommandStart int
GoalCommandStart says whether a command left the goal ready to be pursued right now, and how the turn that follows should be described.
const ( // GoalStartNone: nothing to start — the command read, paused, cleared or // edited the goal. GoalStartNone GoalCommandStart = iota // GoalStartFresh: a brand-new goal that has never run a turn (create, // replace). The TUI announces this one as "Goal starts". GoalStartFresh // GoalStartResumed: an existing goal picking back up after a pause. The // TUI announces it as "Goal continues", the same word a turn-end // continuation uses. GoalStartResumed )
func GoalCommand ¶ added in v1.7.0
func GoalCommand(s *Session, args string) (reply string, start GoalCommandStart)
GoalCommand applies a "/goal …" command to the session and returns a plain text reply — the web composer's surface, which renders it as a scrollback notice. The grammar matches the TUI except `edit`, which takes the new objective inline: the web has no input-prefill to hand the current objective back for editing.
start reports whether the command left the goal ready to be pursued right now — created, replaced, or resumed. The TUI starts the continuation turn itself for exactly these three (startGoalNow); transports that can kick an idle turn gate on it to match. It says nothing about whether a turn should actually run: that stays GoalContinuation's call.
The TUI keeps its own richer dispatcher (prefilled edit, styled summary); the semantics here and there must stay aligned.
type GoalStatus ¶ added in v1.6.1
type GoalStatus string
GoalStatus is the lifecycle state of a session goal. Ownership is the core invariant: the user sets active/paused and clears; the model may only mark complete or blocked (via the update_goal tool); the system sets budget_limited (token budget crossed) and usage_limited (provider quota/rate-limit hit during goal-driven work).
const ( GoalActive GoalStatus = "active" GoalPaused GoalStatus = "paused" GoalBlocked GoalStatus = "blocked" GoalUsageLimited GoalStatus = "usage_limited" GoalBudgetLimited GoalStatus = "budget_limited" GoalComplete GoalStatus = "complete" )
type History ¶
type History struct {
// contains filtered or unexported fields
}
History is the in-memory conversation log for one session. Concurrent-safe because the Web UI and the agent run loop touch it from different goroutines in later milestones.
History does NOT include the system prompt — providers carry that out-of-band (Anthropic's top-level `system` field, OpenAI's first message with role "system"). Keep the system prompt on the Agent struct or as a constructor arg.
func (*History) FindSystemMsg ¶
FindSystemMsg returns the first system message index, or -1.
func (*History) ReplaceAll ¶
ReplaceAll atomically replaces the entire message list. Used by compaction to rebuild history from summary + recent messages.
func (*History) Reset ¶
func (h *History) Reset()
Reset drops all messages. Intended for "start a new session" UX.
func (*History) RewriteDirty ¶
RewriteDirty reports whether history has been rewritten (any non-append mutation) since the flag was last consumed by takeRewriteDirty. Callers use it as a cheap "do I need to re-sync" check; it does not clear the flag.
func (*History) Snapshot ¶
Snapshot returns a copy of the message slice safe to iterate without holding the lock. The returned slice's backing array is fresh; callers can mutate it.
func (*History) TruncateTo ¶
TruncateTo keeps only the first n messages. Used by overflow recovery to pop messages from tail.
func (*History) UpdateMessage ¶ added in v1.15.10
UpdateMessage applies mutate to the i-th message in place under the write lock and marks history rewritten, so the next Session.Save rewrites the file instead of appending. Out-of-range indices are a no-op.
Snapshot hands out copies, so a caller that walks a snapshot and wants a change to stick has to come back through here with the index it saw. Used by the pre-send image transform to cache a description onto the original block.
type ImageData ¶
type ImageData struct {
MIMEType string // e.g. "image/jpeg", "image/png"
Data []byte // raw file bytes
}
ImageData holds raw image bytes and their MIME type for multimodal uploads.
type ImageDescriber ¶ added in v1.15.10
type ImageDescriber interface {
// Active reports whether descriptions are needed right now — false when
// the primary model can see images for itself, in which case image blocks
// travel to the provider untouched. Consulted once per turn rather than
// captured at construction, so a /model switch takes effect immediately.
Active() bool
// Describe returns a text rendering of one image. The returned string goes
// into the conversation verbatim (wrapped in a short attribution line), so
// it should read as prose or structured text, not as a raw API envelope.
Describe(ctx context.Context, img ImageData) (string, error)
}
ImageDescriber renders images as text so a model that can't accept image input still learns what an image contains. app.NewVisionDescriber builds the real one; the agent only knows this interface.
type Inbox ¶
type Inbox struct {
// contains filtered or unexported fields
}
Inbox is a thread-safe queue for user messages that arrive while a turn is running. It mirrors Ruby octo's @inbox: messages accumulate here and are drained into history at the start of each loop iteration, before the LLM call. This keeps mid-turn input handling simple and avoids the complexity of merging steer text into tool_result messages.
func (*Inbox) Drain ¶
Drain returns all queued items and clears the inbox. Returns nil when nothing is queued. Called from the loop goroutine at iteration start.
func (*Inbox) Enqueue ¶
Enqueue adds a text-only message to the inbox. Empty/whitespace-only messages are ignored. Safe to call from any goroutine.
func (*Inbox) EnqueueWithBlocks ¶
func (ib *Inbox) EnqueueWithBlocks(msg string, blocks []ContentBlock)
EnqueueWithBlocks adds a message with optional content blocks to the inbox. Empty/whitespace-only text is ignored, but a non-empty block list with empty text is accepted (image-only steer). Safe to call from any goroutine.
func (*Inbox) HasPending ¶
HasPending reports whether any messages are queued.
func (*Inbox) Remove ¶
Remove deletes the last queued item whose text equals msg and reports whether one was removed. It is used to retract a steer message that hasn't been drained yet: matching by value (last occurrence) means a background notice enqueued between submit and retract doesn't shift the target. Returns false when the message is no longer queued (the loop already drained it) — the caller must then treat it as committed, not retractable.
type InboxItem ¶
type InboxItem struct {
Text string
Blocks []ContentBlock
}
InboxItem is one queued user message, optionally carrying content blocks (e.g. images pasted in the TUI) that should ride on the same turn.
type LowEffortSender ¶ added in v1.8.4
LowEffortSender is implemented by a Sender that can produce a cheaper variant of itself with reasoning effort capped to a small, fast budget. Suggest and GenerateTitle use this as a fallback when NoReasoningSender isn't available: the throwaway call deliberately reuses the turn's own Sender (so the request shares the main conversation's prompt-cache prefix — see Suggest's doc comment), but that Sender carries whatever reasoning_effort the session happens to be configured with. A session running "high"/"max" would otherwise pay the model's full reasoning budget just to produce a one-line suggestion, for no benefit — and on a slower provider this reliably exceeds the throwaway call's timeout. LowEffort caps effort to "low" rather than disabling it outright, so the request shape stays consistent with earlier turns that may already carry thinking blocks in history.
type Message ¶
type Message struct {
Role Role `json:"role"`
Content string `json:"content,omitempty"`
Blocks []ContentBlock `json:"blocks,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
}
Message is a single turn in the conversation.
Content carries plain text for simple turns. Blocks carries the richer multi-part form used when the assistant performs tool calls or when the user returns tool results. When both fields are set the provider adapter should prefer Blocks; when only Content is set the adapter encodes it as a single text block.
func NewAssistantMessage ¶
NewAssistantMessage constructs a Message with RoleAssistant and a current timestamp. If content is empty, it falls back to a non-empty placeholder so the message is valid for providers (Anthropic rejects empty assistant content).
func NewSystemMessage ¶
NewSystemMessage constructs a Message with RoleSystem.
Note: Anthropic's Messages API takes the system prompt as a top-level field rather than as a role within the messages array; the agent's provider adapter is responsible for that translation.
func NewToolResultMessage ¶
func NewToolResultMessage(results []ContentBlock) Message
NewToolResultMessage constructs a user Message that carries tool execution results back to the model. Each element of results must be a tool_result block whose ToolUseID matches a tool_use block in the preceding assistant message.
func NewToolUseMessage ¶
func NewToolUseMessage(blocks []ContentBlock) Message
NewToolUseMessage constructs an assistant Message whose content is a slice of blocks that may include tool_use blocks. The blocks slice typically contains one or more tool_use blocks (and optionally a preceding text block with the model's reasoning).
func NewUserMessage ¶
NewUserMessage constructs a Message with RoleUser and a current timestamp.
type NoReasoningSender ¶ added in v1.12.14
NoReasoningSender is implemented by a Sender that can produce a variant of itself with reasoning disabled entirely. GenerateTitle and Suggest prefer this over LowEffortSender: a 6-word title / one-line follow-up suggestion needs no reasoning at all, and even "low" reasoning can consume the tight token budget or time out. If a sender does not implement this interface, both calls fall back to LowEffortSender instead.
type PermissionGate ¶
type PermissionGate interface {
// Check reports whether the named tool call may run. reason is a
// human/LLM-readable explanation, surfaced in the tool_result when
// allowed is false; it may be empty when allowed is true.
Check(ctx context.Context, name string, input map[string]any) (allowed bool, reason string)
}
PermissionGate decides whether a tool call may proceed. The agent loop consults the gate (if one is set on the Agent) immediately before executing each tool_use block. A denied call never reaches the executor; instead the loop synthesises a tool_result with IsError=true carrying the reason, so the LLM sees the denial and can adapt (suggest an alternative, ask the user to whitelist, etc.) rather than the run aborting.
Implementations own the interaction model for "ask" decisions: a CLI gate prompts the user synchronously, while a non-interactive (server / IM) gate resolves ask → deny. The agent package stays ignorant of how the decision is reached — it only sees the final allow/deny.
type Reply ¶
type Reply struct {
Content string
Blocks []ContentBlock
Model string
StopReason string
InputTokens int
OutputTokens int
// Cache accounting for this call (0 when the backend reports none).
// CacheReadTokens is input served from cache; CacheWriteTokens is input
// written into the cache this turn (Anthropic only).
CacheReadTokens int
CacheWriteTokens int
}
Reply is the agent-level view of a provider response. It deliberately mirrors provider.Response field-for-field (same names, same types) but lives in this package so users of the agent API don't have to import provider.
Blocks is populated when the provider returns content blocks — in particular when stop_reason=="tool_use", Blocks will contain the tool_use blocks that the agentic loop should dispatch.
type Role ¶
type Role string
Role is the message author role, mirroring the role string used by both supported LLM API protocols (Anthropic Messages, OpenAI Chat Completions).
type Sender ¶
type Sender interface {
SendMessages(ctx context.Context, model, system string, messages []Message, maxTokens int) (Reply, error)
}
Sender is the minimal slice of provider.Provider the Agent depends on: it accepts a model, system prompt, messages, and optional max-tokens, and returns the assistant's text reply.
Declaring this interface here (rather than depending on provider.Provider directly) keeps the agent package free of an import on provider, which in turn keeps the dependency graph one-directional: provider → agent, never the other way. The chat subcommand and any future caller is responsible for adapting a provider.Provider into a Sender (trivial — provider's Request/Response already match).
type Session ¶
type Session struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Model string `json:"model"`
System string `json:"system,omitempty"`
// ComposedSystem/ComposedLeanSystem are the fully-composed system prompt
// (base + env + skills + mcp + memory + profile/user/project + System),
// frozen by SetComposedSystem the first time a turn builds this session
// and reused by every later turn — see SetComposedSystem's doc comment
// for why. Empty for a session that hasn't taken its first turn yet, and
// for every session predating this field (it freezes on its next turn).
//
// ComposedForModel is the model the freeze was composed against. The MCP
// tools manifest baked into the prompt (see tools.MCPManifestFor) depends
// on the model's context window — the same MCP tool set can render a
// manifest under a small-window model but not a large one, or vice versa.
// A mid-session model switch (session.SetModelConfig, IM's /model) would
// silently strand the frozen prompt's manifest out of sync with the
// per-turn tools array (which is always computed fresh for the current
// model — see registry.go's defaultToolsFor), so SetComposedSystem
// re-freezes instead of no-op-ing when the model at call time differs
// from this field.
//
// ComposedForCWD is the same idea for the other input that can change
// under a live session: the working directory is baked into the prompt's
// env context ("- Working directory: …"), so retargeting a session's
// directory — or moving it into a project with a different one — must
// re-freeze or the model keeps reading a path its tools no longer run in.
// Empty for sessions frozen before the field existed, which simply
// re-freezes them once on their next turn.
//
// ComposedForSourceDirs is the third such input: a hash of the owning
// project's mounted source folders (and output-dir marker), which the env
// context bakes into the prompt. Mounting or unmounting a folder must
// re-freeze on the next turn. Empty for task sessions (no project) AND for
// sessions written before the field existed — deliberately the same value,
// so pre-existing sessions do not re-freeze once for nothing.
ComposedSystem string `json:"composed_system,omitempty"`
ComposedLeanSystem string `json:"composed_lean_system,omitempty"`
ComposedForModel string `json:"composed_for_model,omitempty"`
ComposedForCWD string `json:"composed_for_cwd,omitempty"`
ComposedForSourceDirs string `json:"composed_for_source_dirs,omitempty"`
Title string `json:"title,omitempty"`
Source string `json:"source,omitempty"` // how the session was created: "" (manual) | "cron" | "channel" | "setup"
// AgentID is the ID of the agent profile (agentprofile.Profile) that owns
// this session. Empty means the default agent — also the value for every
// session predating multi-agent, so legacy files need no migration.
AgentID string `json:"agent_id,omitempty"`
// ModelConfig is the model string of the config entry this session is bound
// to. Empty means "the default entry at turn time" — also the value for
// every session predating per-session model binding. (Sessions written
// before models were keyed by model may carry a legacy entry name here; it
// simply fails to match and falls back to the default sender.)
ModelConfig string `json:"model_config,omitempty"`
// WorkingDir is the session's own working directory: the cwd its tools run
// in, the root its project hooks/skills are discovered from, and the path
// shown in its env context. Empty means "the server's launch directory at
// turn time" — also the value for every session predating per-session
// working dirs. Set via the Web UI's PATCH …/working_dir and persisted so a
// resumed session lands back in the same place.
WorkingDir string `json:"working_dir,omitempty"`
// PermissionMode is this session's own permission mode ("interactive" |
// "auto" | "strict"), snapshotted from the global default at creation
// time and independent of it afterward — changing the global default
// (Settings → default model) only seeds NEW sessions; changing a
// session's own mode (the Web UI's composer toggle) never touches the
// global default or other sessions. Empty means "the global default at
// turn time" — also the value for every session predating per-session
// modes. Set via the Web UI's PATCH …/permission_mode and persisted so a
// resumed session keeps the mode it was left in.
PermissionMode string `json:"permission_mode,omitempty"`
// LastContextTokens is the real input-token count of the most recent model
// request in this session — how full the context window was as of the last
// turn. Persisted so an idle or resumed session (no live Agent in memory)
// reports its true context usage instead of a transcript estimate that omits
// the system-prompt/tools overhead. 0 for sessions predating this field or
// that never completed a turn with a real token count. Updated once per turn
// via SetLastContextTokens.
LastContextTokens int `json:"last_context_tokens,omitempty"`
Messages []Message `json:"messages"`
// Dir overrides the default ~/.octo/sessions location. Empty means use the
// default. Not serialized — it's a runtime override.
Dir string `json:"-"`
// BoundEntry is the entry (cli | tui | web | api | channel | cron | setup)
// that currently owns the session. Empty means unbound. Persisted so a
// resumed session remembers where it was created / last used.
BoundEntry string `json:"bound_entry,omitempty"`
// BoundAt records when BoundEntry was last set, used for diagnostics when
// another entry tries to take over.
BoundAt time.Time `json:"bound_at,omitempty"`
// LeaseEntry/LeaseExpires implement a cross-process "in-flight" lock. A
// turn writes a short-lived lease before starting and clears it on finish;
// another process loading the session can see the lease and refuse to steal
// the binding while it is still valid. The lease is stored as an append-only
// "lease" record so it can be updated without rewriting the whole file.
LeaseEntry string `json:"-"`
LeaseExpires time.Time `json:"-"`
// InFlight counts active turns on this session within the current process.
// It is not persisted; use LeaseEntry/LeaseExpires for cross-process checks.
InFlight int `json:"-"`
// HookStarted records that the SessionStart hook has fired for this session.
// Persisted in the meta line and shared across all three transports, so a
// re-attach resumes (SessionStart source=resume) rather than starting over.
// Set via MarkHookStarted.
HookStarted bool `json:"hook_started,omitempty"`
// BranchedFrom records the session id this session was branched from.
// Empty means the session was created normally. Set via BranchFrom and
// persisted in the meta line so the UI can render a "branched from" label.
BranchedFrom string `json:"branched_from,omitempty"`
// Goal is the session's persistent objective (at most one). Guarded by mu —
// the turn goroutine accounts usage into it while user commands mutate it
// from other goroutines. Mutate only through the goal methods (goal.go);
// they keep the status invariants and persist via append-only "goal"
// records plus the meta header on rewrites.
Goal *Goal `json:"goal,omitempty"`
// contains filtered or unexported fields
}
Session is a named conversation that persists to disk as a JSONL transcript (one record per line) under ~/.octo/sessions/<id>.jsonl. The first line is a meta record; each subsequent line is one message. This lets a turn be saved by APPENDING only its new messages rather than rewriting the whole file — the per-turn cost is O(new messages), not O(total history).
"Last updated" is taken from the file's mtime rather than a stored field, so the append path never has to rewrite an earlier line.
func BranchFrom ¶ added in v1.12.16
BranchFrom creates a new session branched from s, copying its meta fields (model, system prompt, working dir, permission mode) and the first count messages (Messages[0:count]). The new session's BranchedFrom is set to s.ID so the UI can render a lineage label. The caller is responsible for Save()ing the returned session. count is clamped to [0, len(s.Messages)].
func ListSessions ¶
ListSessions returns up to n most-recently-modified sessions from ~/.octo/sessions/, newest first (by file mtime).
func LoadSession ¶
LoadSession reads ~/.octo/sessions/<id>.jsonl. id may be a bare session id, an id with a .jsonl/.json suffix, or an absolute path to a transcript file.
func NewSession ¶
NewSession creates a Session with an ID derived from the current time plus a random suffix: YYYYMMDD-HHMMSS-xxxxxxxx. The timestamp keeps IDs roughly sortable and human-readable; the 32-bit suffix removes same-second collisions (see B3).
func (*Session) AccountGoalUsage ¶ added in v1.6.1
AccountGoalUsage implements GoalAccountant: it folds a token delta and the wall-clock time elapsed since the last accounting into the goal. Usage accrues while the goal is active or budget_limited (in-flight work on a just-limited goal still costs tokens), but only an active goal *crosses* into budget_limited here. Persistence is best-effort — the mutation is in-memory first and the meta header carries the goal on the next rewrite, so a failed append loses durability, not state.
func (*Session) Bind ¶
Bind binds the session to entry in memory. It does NOT persist the change; callers must Save() before another process can see it. This makes the session file the single source of truth for cross-process binding.
func (*Session) ChunkDir ¶
ChunkDir returns the per-session directory where compaction archives the verbatim originals of folded turns (chunk-NNN.md), so the model can recall them with the read tool. Honors the Dir override like SavePath.
func (*Session) ClearComposedSystem ¶ added in v1.15.6
ClearComposedSystem un-freezes the composed system prompt so the next turn that builds this session (buildAgent / runChannelTurns) recomposes it from the live layers — e.g. after a skill install/toggle the user wants this session to pick up without starting a new one. Backs the /reload command. Unlike SetComposedSystem this is NOT a no-op when already set: it exists specifically to undo a previous freeze. Same append-or-rewrite persistence mechanics; the appended record's omitted (empty) fields mean "not frozen" on replay, same as a session that has never taken a turn.
func (*Session) ClearGoal ¶ added in v1.6.1
ClearGoal deletes the goal. Reports whether one existed.
func (*Session) ClearLease ¶
ClearLease appends an empty lease record, clearing the cross-process in-flight marker.
func (*Session) ConsumeGoalBudgetSteer ¶ added in v1.7.0
ConsumeGoalBudgetSteer implements GoalAccountant; see the interface doc.
func (*Session) ConsumeGoalObjectiveSteer ¶ added in v1.7.1
ConsumeGoalObjectiveSteer implements GoalAccountant; see the interface doc.
func (*Session) CreateGoal ¶ added in v1.6.1
CreateGoal starts a new active goal. It fails when any goal exists — including a finished one; replacing is an explicit separate operation so the model-facing create_goal tool can never silently discard a goal.
func (*Session) DecFlight ¶
func (s *Session) DecFlight()
DecFlight decrements the in-flight turn count, guarding against underflow.
func (*Session) DisplayTitle ¶
DisplayTitle returns the label shown for the session in list views: the generated Title when present, otherwise a snippet of the first user message (so pre-title sessions and not-yet-titled ones are still recognisable), and finally "*Octo Agent" when there's nothing to show.
func (*Session) EditGoalObjective ¶ added in v1.6.1
EditGoalObjective rewrites the objective in place, preserving usage counters and budget. A budget_limited or complete goal re-activates on edit (the user is redefining what done means); other statuses are preserved so editing a paused goal does not silently resume it.
func (*Session) EffectiveAgentID ¶ added in v1.13.1
EffectiveAgentID returns the owning agent profile's ID: "default" for the default agent and for every session predating the AgentID field (kept as a literal here so the agent package stays free of an agentprofile import).
func (*Session) EndsMidTurn ¶
EndsMidTurn reports whether the persisted transcript stops in the middle of a turn: the last message is a user message still awaiting a reply (the initiating input, a tool_result batch, or a mid-turn steer), or an assistant tool_use whose results never landed. A turn that finished — or was interrupted by the user — always ends on a plain assistant text message (finishInterrupted guarantees this), so a mid-turn tail means the process died with the turn in flight: crash, kill, power loss. Callers use it to warn the model that tool side effects from that turn may be unrecorded.
func (*Session) FallbackTitleIfPlaceholder ¶ added in v1.12.17
FallbackTitleIfPlaceholder replaces the "*Octo Agent" placeholder with the first-message snippet when available. Returns the new title, or "" when the title isn't the placeholder or there's no message content to use. Both the TUI and the web server call this when async title generation fails.
func (*Session) GoalContinuation ¶ added in v1.7.0
GoalContinuation reports whether an idle follow-up turn should start for the session's goal and returns the hidden prompt to start it with. Call it after a turn fully completes, when no other input is pending; enqueue the prompt as the next turn's user input.
It owns the continuation policy: only an active goal continues; a continuation turn that accounted zero tokens suppresses further continuations until real token progress or a goal mutation re-arms them (the zero-progress guard — an idle-spinning loop must stop itself); and each hand-out is audited by the next call, so the caller needs no bookkeeping of its own.
func (*Session) GoalContinuationPending ¶ added in v1.7.0
GoalContinuationPending reports whether the most recent turn was started by GoalContinuation and has not been audited yet. Transports use it to tell a failing continuation turn (mark the goal usage_limited on rate-limit errors, so the loop parks itself) from a failing user turn.
func (*Session) GoalSnapshot ¶ added in v1.6.1
GoalSnapshot returns a copy of the session's goal, or ok=false when none is set.
func (*Session) IncFlight ¶
func (s *Session) IncFlight()
IncFlight increments the in-flight turn count. Caller must already hold the binding; the increment is a no-op if the session is unbound.
func (*Session) IsComposedFor ¶ added in v1.15.6
IsComposedFor reports whether the session's system prompt is already frozen for this model / working directory pair — the condition SetComposedSystem itself uses to decide overwrite-vs-no-op, exposed so callers (buildAgent, runChannelTurns) can skip the memory/skills/MCP recompute entirely when the freeze would just be reused rather than replaced.
func (*Session) LeaseActive ¶
LeaseActive reports whether the session has an unexpired turn lease and who holds it.
func (*Session) MarkHookStarted ¶
func (s *Session) MarkHookStarted()
MarkHookStarted records that SessionStart has fired for this session, so a later attach (new process, or a resumed session) resumes rather than starting over. The flag lives in the meta line, so it forces the next Save to rewrite the file — a one-time O(n) cost per session. Idempotent; safe to call every turn. Persistence rides the session layer's existing post-turn Save.
func (*Session) ReplaceGoal ¶ added in v1.6.1
ReplaceGoal discards any existing goal and starts a fresh active one with a new ID and zeroed usage counters. Backs the user-confirmed "/goal <objective>" replace path.
func (*Session) ResetGoalWallClock ¶ added in v1.6.1
func (s *Session) ResetGoalWallClock()
ResetGoalWallClock implements GoalAccountant: it restarts the wall-clock baseline for an accruing goal so time that passed between turns is dropped rather than billed. Called from the turn-start baseline reset; a stopped goal (no baseline) is left alone. A stale mid-turn-creation skip flag is dropped here too — at a turn boundary the token baseline is fresh, so the first accounting must bill normally.
func (*Session) Save ¶
Save persists the session. The common case appends only the messages added since the last Save. The file is rewritten from scratch (an infrequent O(n) event) when the on-disk prefix can't be trusted: a history rewrite observed by SyncFrom (forceRewrite), or an in-memory list shorter than what's on disk — the length check is kept as a belt-and-braces fallback for callers that assign Messages directly instead of going through SyncFrom. With no new messages and a trusted prefix, Save is a no-op, so per-event callers (the server persists mid-turn progress on every agent event) don't touch the file at all between rounds.
func (*Session) SetAgentID ¶ added in v1.15.0
SetAgentID rebinds the session to a different agent profile. Only called while the session still has zero turns (enforced by the caller, the agent_profile PATCH handler) — AgentID is otherwise fixed for the life of the session once a turn has run against it. Same persistence mechanics as SetWorkingDir: append a record when the transcript is already on disk so the change survives without rewriting the file, rewrite when the on-disk prefix is stale, and carry the value in memory for a not-yet-saved session until its first Save folds it into the meta header. Setting the id already in place is a no-op.
func (*Session) SetBoundEntry ¶
SetBoundEntry writes the binding fields directly. Used by persistent stores after reloading the authoritative record from disk.
func (*Session) SetComposedSystem ¶ added in v1.15.6
SetComposedSystem freezes the fully-composed system prompt (base + env + skills + mcp + memory + profile/user/project + System) the first time a turn builds this session, so every later turn reuses the identical string instead of recomposing it from layers that can legitimately change mid- session — the live memory file an agent's own tools just wrote, a skill toggle, a profile edit. Recomposing on any of those would vary the text and invalidate the provider's prompt-cache prefix on that turn (and every turn after, since the changed layer stays changed). This mirrors the CLI/TUI, which compose once per process and never touch System again: a skill or profile edit now takes effect in a new session, not a running one.
model and cwd are the freeze's identity — see ComposedForModel and ComposedForCWD for why each matters. A no-op only when already frozen for THIS exact pair; a call with either different (a mid-session model switch, a retargeted working directory) overwrites the freeze instead, since both are baked into the prompt while the per-turn tools array and tool cwd are always computed fresh — a stale freeze would silently drift out of sync with them. Same append-or-rewrite persistence mechanics as SetPermissionMode.
func (*Session) SetGoalStatus ¶ added in v1.6.1
func (s *Session) SetGoalStatus(status GoalStatus) (Goal, error)
SetGoalStatus applies a status change. Which transitions a caller may request is that caller's contract (slash commands pause/resume, the update_goal tool completes/blocks, the runtime limits); this method owns the invariants that hold regardless of caller: in-flight wall-clock time is accounted first, re-activating starts a fresh wall-clock baseline, an already-over-budget goal cannot re-enter active, and a completed goal cannot be reactivated this way — EditGoalObjective/ReplaceGoal are the only paths back from complete, matching what every UI surface offers a finished goal (edit/clear, never resume).
func (*Session) SetLastContextTokens ¶ added in v1.12.2
SetLastContextTokens records the context-window fill (real input-token count) as of the just-finished turn, so an idle/resumed session reports its true usage without a live Agent. Same append-or-rewrite persistence mechanics as SetPermissionMode; called once per turn, so an unchanged count is a no-op (avoids a redundant append when the context didn't grow).
func (*Session) SetModelConfig ¶
SetModelConfig binds the session to a config entry, identified by its model string (empty = the default entry at turn time), and records that model so the session displays and resumes on the right model. Like SetTitle, it appends a record when the transcript is already on disk so the binding survives without rewriting the file; a switch to the values already in place is a no-op.
func (*Session) SetPermissionMode ¶ added in v1.8.1
SetPermissionMode records the session's own permission mode. Same persistence mechanics as SetWorkingDir: append a record when the transcript is already on disk so the change survives without rewriting the file, rewrite when the on-disk prefix is stale, and carry the value in memory for a not-yet-saved session until its first Save folds it into the meta header. Setting the mode already in place is a no-op.
func (*Session) SetTitle ¶
SetTitle records a short human-readable title for the session. It updates the in-memory field and, if the transcript already exists on disk, appends a title record so the title survives without rewriting the file. A fresh session whose file isn't written yet just carries the title until the first Save folds it into the meta header. Calling with an empty or unchanged title is a no-op.
func (*Session) SetWorkingDir ¶ added in v1.6.0
SetWorkingDir records the session's working directory. Same persistence mechanics as SetModelConfig: append a record when the transcript is already on disk so the change survives without rewriting the file, rewrite when the on-disk prefix is stale, and carry the value in memory for a not-yet-saved session until its first Save folds it into the meta header. Setting the dir already in place is a no-op.
func (*Session) ShortID ¶
ShortID returns the 8-character abbreviation suitable for CLI display. It's the trailing hex suffix of the full ID — the random part — since the timestamp prefix repeats across same-second sessions and uniqueness comes from the suffix. Same idea as git's short SHA.
func (*Session) SuppressGoalContinuation ¶ added in v1.7.0
func (s *Session) SuppressGoalContinuation()
SuppressGoalContinuation parks the continuation loop without touching the goal itself. Transports call it when a turn was interrupted (the user said stop — continuing immediately would make the loop interrupt-proof) or errored (retrying an erroring turn unprompted is unbounded paid retries). The zero-progress audit can't catch either case: an aborted or errored turn usually still accounted partial tokens. The standard re-arms apply — real token progress from a later user turn, or any goal mutation.
func (*Session) SyncFrom ¶
SyncFrom copies the current messages from h into the session. Call this before Save to flush the latest turns. When the history was rewritten since the last sync (compaction, repair, popLast), the next Save rewrites the file instead of appending — see History.rewritten.
func (*Session) TruncateTo ¶ added in v1.12.16
TruncateTo keeps only the first n messages and forces the next Save to rewrite the whole file (the on-disk prefix no longer matches). Mirrors History.TruncateTo. Used by the edit-message flow, which rewrites a message in place and drops everything after it.
func (*Session) Unbind ¶
Unbind releases the session from entry in memory. No-op if not bound to entry. Callers must Save() to publish the change.
func (*Session) UsedTools ¶
UsedTools reports whether any assistant message in the session emitted at least one tool_use block. Used by the chat resume path to decide whether to auto-enable --tools — without that, a resumed session whose history contains tool_use blocks would be sent to the model with no tools array, and the model (seeing prior tool calls in the conversation but unable to make new structured ones) falls back to emitting tool calls as text. That looks like garbled XML to the user.
type StreamingSender ¶
type StreamingSender interface {
Sender
StreamMessages(
ctx context.Context,
model, system string,
messages []Message,
maxTokens int,
onChunk func(textDelta string),
onThinking func(thinkingDelta string),
) (Reply, error)
}
StreamingSender extends Sender with the ability to deliver the assistant reply chunk-by-chunk via a callback while the upstream stream is open.
The agent type-asserts its Sender to StreamingSender at the start of TurnStream; if the underlying provider doesn't implement it, TurnStream falls back to the buffered Sender.SendMessages and invokes onChunk once with the full content for callers that don't want to branch on capability.
type StreamingToolExecutor ¶
type StreamingToolExecutor interface {
ToolExecutor
ExecuteStream(
ctx context.Context,
name string,
input map[string]any,
progress func(chunk string),
) (ToolResult, error)
}
StreamingToolExecutor is an optional extension to ToolExecutor: tools that produce incremental output (e.g. a long shell command writing stdout line by line) can implement ExecuteStream and surface chunks as they happen.
The agent loop type-asserts the executor at dispatch time. If the executor supports streaming AND the caller provided an EventHandler, the loop calls ExecuteStream and forwards each chunk as an EventToolProgress event. Otherwise the loop falls back to Execute.
progress may be nil; implementations should treat a nil progress callback as equivalent to non-streaming Execute. The (ToolResult, error) return is the FULL aggregated result (same contract as Execute) — progress chunks are for UI/observability only.
type ThinkingDeltaFunc ¶
type ThinkingDeltaFunc func(thinkingDelta string)
ThinkingDeltaFunc receives fragments of a reasoning model's thinking trace as they stream in, before the visible reply. May be nil; implementations treat nil as "don't surface reasoning" and skip the callback.
type ToolDefinition ¶
type ToolDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]any `json:"parameters"` // JSON Schema object
}
ToolDefinition describes a tool the LLM may invoke. The Parameters field must be a valid JSON Schema "object" definition; most tools only need "type", "properties", and "required".
type ToolExecutor ¶
type ToolExecutor interface {
Execute(ctx context.Context, name string, input map[string]any) (ToolResult, error)
}
ToolExecutor dispatches tool calls on behalf of the agentic loop. Each implementation maps a tool name to a function; unknown names should return an error so the LLM sees a clean error result rather than a panic.
type ToolInputDeltaFunc ¶
type ToolInputDeltaFunc func(toolID, toolName, partialJSON string)
ToolInputDeltaFunc receives raw JSON fragments of a tool_use block's arguments as they stream in. Fragments concatenate to form the final JSON object. May be nil; implementations should treat nil as "don't surface tool-input deltas" and skip the callback.
type ToolResult ¶
type ToolResult struct {
Text string // required textual summary
Blocks []ContentBlock // optional rich content (images, etc.)
// UI is an optional structured rendering of the result for UI consumers
// (the web frontend's rich result cards). It never reaches the model —
// only Text/Blocks do. It travels on the tool_result ContentBlock (and
// therefore persists with the session) and on the EventToolDone event.
UI any
}
ToolResult is the return value from a tool execution. Text is the required textual summary (shown in the UI and sent to the model as the primary result). Blocks holds optional rich content — images for multimodal models, structured data, etc. — that the provider adapter serialises into the vendor-specific wire format.
type ToolSender ¶
type ToolSender interface {
Sender
SendMessagesWithTools(
ctx context.Context,
model, system string,
messages []Message,
maxTokens int,
tools []ToolDefinition,
) (Reply, error)
}
ToolSender extends Sender with a tool-aware variant that carries tool definitions alongside the messages. Implementations return the full content-block list (including tool_use blocks) in Reply.Blocks.
type ToolStreamingSender ¶
type ToolStreamingSender interface {
ToolSender
StreamMessagesWithTools(
ctx context.Context,
model, system string,
messages []Message,
maxTokens int,
tools []ToolDefinition,
onChunk func(textDelta string),
onToolDelta ToolInputDeltaFunc,
onThinking ThinkingDeltaFunc,
) (Reply, error)
}
ToolStreamingSender extends ToolSender and StreamingSender with a streaming tool-aware variant. Implementations stream text deltas via onChunk, stream tool-argument JSON fragments via onToolDelta (optional, may be nil), and accumulate tool_use blocks. The final Reply carries Blocks for dispatch.