Documentation
¶
Overview ¶
Package loop also provides conversation compaction helpers.
Package loop drives one bee turn: build prompt, stream provider, dispatch tools, persist to rollout, recurse until the model stops.
Index ¶
- Constants
- Variables
- func ShouldAutoCompact(sys string, msgs []types.Message, budget int, threshold float64) bool
- func ShouldAutoCompactWithUsage(sys string, msgs []types.Message, actualInputTokens, budget int, ...) bool
- func StripSchemaDescriptionsForProfile(spec map[string]any, profile string) map[string]any
- type CompactStats
- type Engine
- func (e *Engine) Compact(ctx context.Context) ([]types.Message, CompactStats, error)
- func (e *Engine) PrepareResume(ctx context.Context) (CompactStats, bool, error)
- func (e *Engine) Run(ctx context.Context, userMsg string) (RunResult, error)
- func (e *Engine) RunWithContent(ctx context.Context, content []types.ContentBlock) (RunResult, error)
- func (e *Engine) RunWithContentDisplay(ctx context.Context, content []types.ContentBlock, display string) (RunResult, error)
- type EscalateError
- type FormatStrikeError
- type KnowledgeStore
- type Mode
- type Observation
- type PerToolFailureError
- type RecapResult
- type RepeatStreamError
- type RunResult
- type ToolCallSignature
- type TruncatedStreamError
- type TwoStrikeError
Constants ¶
const MaxIterations = 50
MaxIterations is the default safety cap: if the model keeps emitting tool_use past this many turns, abort. Override per-engine via Config.MaxIterations (0 = use this default).
const PreserveTail = 4
PreserveTail is the number of trailing messages kept verbatim during compaction.
Variables ¶
var ErrEscalate = errors.New("loop: model escalated to user")
ErrEscalate is the typed sentinel for the `escalate` tool. callers match via errors.Is to detect "the model chose to stop and ask the user".
var ErrFormatStrike = errors.New("loop: format strike — model wedged on malformed envelope")
ErrFormatStrike indicates the model emitted text that LOOKED like a tool call (XML or JSON envelope) N times in a row without the parser recognizing any of them. signals the model is wedged on a malformed envelope shape that no amount of nudging will fix — bail and let the user / wrapper switch model or prompt.
var ErrPerToolFailureCap = errors.New("loop: tool failed beyond per-tool cap")
ErrPerToolFailureCap indicates a single tool name has errored K times in a row regardless of args. signals the model is wedged on a specific tool.
var ErrRepeatStream = errors.New("loop: stream stuck repeating itself")
ErrRepeatStream indicates the model's stream was cut for degenerate repetition (the same phrase looped) N turns in a row — it's wedged in a token loop that nudging won't fix. bail and let the user / wrapper switch model.
var ErrTruncatedStream = errors.New("loop: stream dropped mid-output repeatedly")
ErrTruncatedStream indicates the provider stream dropped mid-output (a transient network error AFTER content already streamed) N turns in a row. The first such drops are recovered — bee keeps the partial turn and nudges the model to continue — but a persistent drop means the connection is dead, so bail rather than reconnect forever.
var ErrTwoStrike = errors.New("loop: tool call failed twice in a row")
ErrTwoStrike indicates the same tool call (name + args) errored twice in a row. caller should stop looping and surface the cause to the user.
Functions ¶
func ShouldAutoCompact ¶
ShouldAutoCompact returns true if assembled prompt + history exceeds budget * threshold. budget<=0 disables. Estimate-only variant — kept for callers that don't have a recent provider usage report. Prefer ShouldAutoCompactWithUsage when an EventDone usage is available.
func ShouldAutoCompactWithUsage ¶
func ShouldAutoCompactWithUsage(sys string, msgs []types.Message, actualInputTokens, budget int, threshold float64) bool
ShouldAutoCompactWithUsage trips when input-token usage crosses budget*threshold. When actualInputTokens > 0 (real value from provider's last EventDone usage) we use it directly — most accurate signal we have and works for any provider that reports usage. Falls back to a heuristic estimate over sys + every content block (text, thinking, tool_use input, tool_result content) when no live count is available.
budget<=0 or threshold<=0 disables.
func StripSchemaDescriptionsForProfile ¶
StripSchemaDescriptionsForProfile removes all "description" keys deep in a tool's parameter schema when the profile is "tiny". Wire-format param descriptions aren't part of the token budget the user sees, but they DO count against the provider's context window: typically ~600 tokens for the full toolset. Tiny models don't need them, the one-line tool summary in the system prompt manifest is enough.
Types ¶
type CompactStats ¶
type CompactStats struct {
BeforeMsgs int
AfterMsgs int
BeforeTokens int
AfterTokens int
Duration time.Duration
}
CompactStats reports what a Compact call achieved. Duration spans the LLM summarization plus token accounting. Token figures are estimates derived from estimateMessageTokens so they line up with the auto-compact trigger heuristic.
func Compact ¶
func Compact(ctx context.Context, p llm.Provider, model string, msgs []types.Message) ([]types.Message, CompactStats, error)
Compact summarizes msgs[:-PreserveTail] using provider into a single user message containing "[compacted history]\n<summary>" and returns the new slice plus stats describing the size delta and elapsed time. Returns the input unchanged if it has PreserveTail or fewer messages.
type Engine ¶
type Engine struct {
Provider llm.Provider
Tools *tools.Registry
Skills *skills.Registry
Memory KnowledgeStore
Sessions *session.Rollout
Cfg config.Config
Cwd string
Stdout io.Writer
// SteerCh, when non-nil, is drained at the top of each iteration to
// inject mid-turn user steering between LLM rounds.
SteerCh chan string
// StreamCh, when non-nil, receives every text delta produced by the
// provider in lieu of writing them to Stdout. The TUI uses this to
// route deltas through bubbletea so the alt-screen isn't corrupted.
// Sends are non-blocking — a slow consumer drops deltas rather than
// stalling the model stream.
StreamCh chan string
// ThinkCh, when non-nil, receives every chain-of-thought delta as it
// arrives. Separate from StreamCh so the TUI can render reasoning
// live in a dimmed/italic block above the answer instead of waiting
// for the whole thinking buffer to flush at end-of-stream. Sends are
// non-blocking — slow consumer drops deltas.
ThinkCh chan string
// LiveMsgCh, when non-nil, receives every assistant + tool message as
// it's persisted, so a UI can render tool_use / tool_result cards in
// real time instead of only after Run returns. User messages are NOT
// sent (the caller's UI already shows an optimistic copy). Sends are
// non-blocking — a stalled consumer doesn't stall the loop.
LiveMsgCh chan types.Message
// WarnCh, when non-nil, receives transient operational notices: stream
// retries, watchdog stalls, etc. The TUI fades them as a small chrome
// line so the user knows something happened without the turn aborting.
// Sends are non-blocking — a slow consumer drops notices.
WarnCh chan string
// JSONEmitter, when non-nil, receives one NDJSON event per significant
// happening (text delta, tool use, tool result, done, error) and
// suppresses the human-readable text-delta write to Stdout.
JSONEmitter *jsonmode.Emitter
// Costs, when non-nil, accumulates per-turn usage/dollar events. The
// TUI reads from the same tracker to drive the top-bar total and the
// /cost monitor pane.
Costs *cost.Tracker
// InitialMessages, when non-nil, seeds the in-memory message list at
// the start of each Run so the model receives prior turns as context.
// The TUI refreshes this per submit; `bee back <id>` sets it from disk.
// Messages here are NOT re-appended to the rollout — they're already on
// disk or never were (caller's responsibility).
InitialMessages []types.Message
// Rebuild, when non-nil, is invoked by the TUI after a mid-session
// provider/model switch (`/model` or the picker). The closure owns
// re-creating Provider + Memory from the current Cfg so the next turn
// talks to the new backend instead of the original one cached at Engine
// construction. nil = no live switching (headless, hive workers).
Rebuild func(*Engine) error
// OnceAllowTools force-allows plan-only tools (e.g. ask_user) for the next
// Run, regardless of the active mode. A prompt skill sets it from its
// frontmatter `tools` list so /plan can ask the user even in edit/auto.
// Only plan-only tools are honoured — it can't re-enable write/bash that
// plan mode legitimately strips. Cleared at the top of each Run.
OnceAllowTools []string
// contains filtered or unexported fields
}
Engine wires every component bee needs to run one or many turns.
func (*Engine) Compact ¶
Compact summarizes the session's older messages and returns the compacted slice plus stats. Caller is responsible for replacing the in-memory message list (e.g. TUI scrollback / InitialMessages) so the next turn sees the shorter history. The raw on-disk log stays append-only, but a checkpoint marker is appended so resume (session.ReadResume) reconstructs this shortened view instead of replaying the full history.
func (*Engine) PrepareResume ¶
PrepareResume compacts a resumed session's seeded history when it already exceeds the compaction threshold, BEFORE the first turn runs. Without this, the first post-resume turn ships the whole history to the model in one shot — on a slow local model that is many minutes of prompt processing that reads as a hang. Returns the stats and whether compaction actually ran; on success e.InitialMessages is replaced with the shortened history.
func (*Engine) Run ¶
Run executes the agent loop until the model emits a stop without tool use, or MaxIterations is hit. The user message is appended to the session. Thin wrapper around RunWithContent for the text-only call path.
func (*Engine) RunWithContent ¶
func (e *Engine) RunWithContent(ctx context.Context, content []types.ContentBlock) (RunResult, error)
RunWithContent is Run with a pre-built content slice. Used by the TUI when staging multimodal input (e.g. images via Ctrl+I) so the user message can carry text + image blocks together.
func (*Engine) RunWithContentDisplay ¶
func (e *Engine) RunWithContentDisplay(ctx context.Context, content []types.ContentBlock, display string) (RunResult, error)
RunWithContentDisplay is RunWithContent with a render-only display label for the user turn. When display is non-empty the stored user message shows it in the TUI instead of Content (slash skills: typed command shown, expanded body sent). Empty display behaves exactly like RunWithContent.
type EscalateError ¶
EscalateError wraps the escalate tool's payload so callers (TUI, headless run) can surface the model's reason + suggested-next-action in the exit message instead of just a generic sentinel.
func (*EscalateError) Error ¶
func (e *EscalateError) Error() string
func (*EscalateError) Is ¶
func (e *EscalateError) Is(target error) bool
func (*EscalateError) Unwrap ¶
func (e *EscalateError) Unwrap() error
type FormatStrikeError ¶
type FormatStrikeError struct {
Streak int
}
FormatStrikeError wraps a format-strike bail with the streak length so callers can render "model emitted malformed envelopes 3x — switch model".
func (*FormatStrikeError) Error ¶
func (e *FormatStrikeError) Error() string
func (*FormatStrikeError) Is ¶
func (e *FormatStrikeError) Is(target error) bool
func (*FormatStrikeError) Unwrap ¶
func (e *FormatStrikeError) Unwrap() error
type KnowledgeStore ¶
type KnowledgeStore interface {
Query(ctx context.Context, query string, recentTools []string) ([]knowledge.Record, error)
}
KnowledgeStore abstracts knowledge selection so the engine doesn't pull in the full knowledge package (and tests can stub it).
type Mode ¶
type Mode string
Mode gates engine behavior per Run.
ModePlan: read-only tools, agent produces plan only, no edits. ModeAuto: classifier picks plan|edit per turn (default). ModeEdit: full tool surface, dangerous commands still prompt. ModeYolo: full tool surface, approvable commands auto-approved (no prompt).
func ClassifyMode ¶
ClassifyMode runs a cheap side-LLM call against userText to pick plan|edit. On any error or ambiguous response, falls back to ModeEdit. classifier is a separate function so callers can stub it in tests.
type Observation ¶
type Observation struct {
Sig ToolCallSignature
RepeatCount int // how many times this exact Sig has fired this Run
ConsecutiveSameToolFailures int // streak of failures on the same tool name (any args)
ConsecutiveSameSigFailures int // streak of failures on the same (tool,args) sig
IsTwoStrike bool // same Sig failed twice in a row → nudge (no longer bails)
}
Observation summarizes what the tracker learned about a freshly observed (ToolUse, isError) pair. Caller decides how to react.
type PerToolFailureError ¶
PerToolFailureError wraps a per-tool-cap bail with the offending tool name and the streak length so callers can surface "bash failed 8x in a row".
func (*PerToolFailureError) Error ¶
func (e *PerToolFailureError) Error() string
func (*PerToolFailureError) Is ¶
func (e *PerToolFailureError) Is(target error) bool
func (*PerToolFailureError) Unwrap ¶
func (e *PerToolFailureError) Unwrap() error
type RecapResult ¶
RecapResult carries the parsed recap line plus any error/skip cause so callers can render a visible diagnostic when generation fails or the model skipped. Text is empty when Err is set or Skipped is true.
func GenerateRecap ¶
func GenerateRecap(ctx context.Context, p llm.Provider, model string, msgs []types.Message) RecapResult
GenerateRecap calls the same provider/model with a short summarising prompt and returns the assistant's recap line. Empty Text + nil Err + Skipped=true means the model emitted the "skip" sentinel or the turn had no assistant text. Stream enabled for parity with classifier — adapters without streaming still aggregate to a single text response.
type RepeatStreamError ¶
type RepeatStreamError struct {
Streak int
}
RepeatStreamError wraps a repetition-loop bail with the streak length so callers can render "model looped its output 3x — switch model".
func (*RepeatStreamError) Error ¶
func (e *RepeatStreamError) Error() string
func (*RepeatStreamError) Is ¶
func (e *RepeatStreamError) Is(target error) bool
func (*RepeatStreamError) Unwrap ¶
func (e *RepeatStreamError) Unwrap() error
type ToolCallSignature ¶
ToolCallSignature identifies a tool call by name + canonical-args hash. Two calls with semantically-equal args (key order, whitespace in JSON stringification) collide on the same Sig.
type TruncatedStreamError ¶
type TruncatedStreamError struct {
Streak int
}
TruncatedStreamError wraps a mid-stream-drop bail with the streak length.
func (*TruncatedStreamError) Error ¶
func (e *TruncatedStreamError) Error() string
func (*TruncatedStreamError) Is ¶
func (e *TruncatedStreamError) Is(target error) bool
func (*TruncatedStreamError) Unwrap ¶
func (e *TruncatedStreamError) Unwrap() error
type TwoStrikeError ¶
type TwoStrikeError struct {
Use types.ToolUse
Class string // tool-error class tag (toolErrNotFound, toolErrTimeout, etc.)
}
TwoStrikeError wraps the offending ToolUse so callers (TUI, headless `bee run`) can surface tool name + args in the exit message.
func (*TwoStrikeError) Error ¶
func (e *TwoStrikeError) Error() string
func (*TwoStrikeError) Is ¶
func (e *TwoStrikeError) Is(target error) bool
Is lets errors.Is(err, ErrTwoStrike) match wrapped variants.
func (*TwoStrikeError) Unwrap ¶
func (e *TwoStrikeError) Unwrap() error
Unwrap surfaces the sentinel for errors.Is chains.
Source Files
¶
- compact.go
- compact_notice.go
- done_signal.go
- edit_verify.go
- escalate_errs.go
- idempotency.go
- mode.go
- profile_tools.go
- recap.go
- repeat_detect.go
- repeat_stream.go
- sandbox_wrap.go
- tool_error.go
- turn.go
- turn_cache.go
- turn_caps.go
- turn_compact.go
- turn_helpers.go
- turn_idempotency.go
- turn_profile_filter.go
- turn_reasoning_dup.go
- turn_recover.go
- turn_repeat.go
- turn_run.go
- turn_stream.go
- turn_tools.go
- turn_warnings.go
- vision_fallback.go