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 BuildHandoff(ctx context.Context, p llm.Provider, model string, msgs []types.Message, ...) (string, error)
- func IsWedge(err error) bool
- func RoleThinking(r Role) llm.Thinking
- func ShouldAutoCompact(sys string, msgs []types.Message, budget int, threshold float64) bool
- func ShouldAutoCompactWithUsage(sys string, msgs []types.Message, actualInputTokens, budget int, ...) bool
- func ShouldResume(err error, res RunResult) (bool, string)
- func StripSchemaDescriptionsForProfile(spec map[string]any, profile string) map[string]any
- type CompactStats
- type EmptyCompletionError
- type Engine
- func (e *Engine) Compact(ctx context.Context) ([]types.Message, CompactStats, error)
- func (e *Engine) Handoff(ctx context.Context, msgs []types.Message, partial, stall string) (string, 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 MaxIterationsError
- type Observation
- type PerToolFailureError
- type RecapResult
- type RepeatStreamError
- type ResumeDecision
- type Role
- type RunResult
- type ToolCallSignature
- type TruncatedStreamError
- type TwoStrikeError
Constants ¶
const MaxIterations = config.DefaultMaxIterations
MaxIterations is the default safety cap: if the model keeps emitting tool_use past this many turns, abort. Per-run override via Config.MaxIterations (0 = unlimited).
const PreserveTail = 4
PreserveTail is the number of trailing messages kept verbatim during compaction.
Variables ¶
var ErrEmptyCompletion = errors.New("loop: model returned empty output repeatedly")
ErrEmptyCompletion indicates the provider returned whitespace-only output — no text, no reasoning, no tool_use — N turns in a row. A nudge couldn't shake it loose, so the model (or its inference template) is producing dead turns; bail and let the user switch model instead of spinning the iter budget. Commonly a thinking-suppression switch the chat template mishandles.
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 ErrMaxIterations = errors.New("loop: hit max iterations")
ErrMaxIterations indicates the loop hit its tool-use round cap without the model signalling done. Not a wedge — the model was making progress, it just ran out of budget — so the watchdog can resume it with a plain "continue".
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 BuildHandoff ¶ added in v0.1.1
func BuildHandoff(ctx context.Context, p llm.Provider, model string, msgs []types.Message, partial, stall string) (string, error)
BuildHandoff renders the prompt a bigger model receives when taking over a stuck small-model run: framing + the original goal (verbatim) + a terse summary of the confused middle (summarized with p/model) + the stall signal + the last PreserveTail turns + any interrupted partial output. stall and partial may be empty. The middle summary uses the same streamed-summary path as Compact, so it should run on the small/fast model BEFORE switching.
func IsWedge ¶ added in v0.1.1
IsWedge reports the "this turn got stuck, a reframed retry is sane" family. This is the exact set cmd/bee/run_goal.go inlines as isWedgedTurn — extracted so the goal loop and the watchdog share one definition. Deliberately excludes ErrTruncatedStream, context.DeadlineExceeded, and ErrMaxIterations: those are handled as their own resume reasons, not as wedges.
func RoleThinking ¶ added in v0.1.1
RoleThinking is the reasoning budget baked into each role, applied when the user hasn't pinned an explicit Thinking override. worker stays cheap, scout digs, queen maxes out for the hive.
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 ShouldResume ¶ added in v0.1.1
ShouldResume is the thin (bool, continuation) wrapper over ClassifyResume.
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 EmptyCompletionError ¶ added in v0.1.1
type EmptyCompletionError struct {
Streak int
}
EmptyCompletionError wraps an empty-completion bail with the streak length.
func (*EmptyCompletionError) Error ¶ added in v0.1.1
func (e *EmptyCompletionError) Error() string
func (*EmptyCompletionError) Is ¶ added in v0.1.1
func (e *EmptyCompletionError) Is(target error) bool
func (*EmptyCompletionError) Unwrap ¶ added in v0.1.1
func (e *EmptyCompletionError) Unwrap() error
type Engine ¶
type Engine struct {
Provider llm.Provider
Tools *tools.Registry
Skills *skills.Registry
Memory KnowledgeStore
Sessions *session.Rollout
// Waggle, when non-nil, observes read-only tool calls so repeated routes can
// be crystallized into reusable exec-skills (procedure memory). nil disables.
Waggle *waggle.Manager
// Replay, when non-nil, follows previously crystallized routes: after a tool
// batch it matches the recent read-only calls against stored waggle prefixes
// and, on a confident match, runs the route's remaining read-only steps off
// the model's path, folding their output into the triggering tool result so
// the model skips the round-trips. Zero prompt cost (matching is in Go). nil
// disables.
Replay *waggle.Replayer
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 role. A prompt skill sets it from its
// frontmatter `tools` list so /plan can ask the user even from worker.
// Only plan-only tools are honoured — it can't re-enable write/bash that a
// read-only turn legitimately strips. Cleared at the top of each Run.
OnceAllowTools []string
// SkipPostureClassifier disables the worker read-only/act classifier for
// this engine, so a worker turn always gets the full tool surface. Set for
// the scripted-provider test harness (the extra side Stream call would
// desync scripted response counts) and for callers that want byte-for-byte
// "always act" worker behavior.
SkipPostureClassifier bool
// 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) Handoff ¶ added in v0.1.1
func (e *Engine) Handoff(ctx context.Context, msgs []types.Message, partial, stall string) (string, error)
Handoff builds a rescue brief from the supplied in-memory transcript using the fast/compaction model and the CURRENT provider. Call this BEFORE switching to the big model so the cheap, already-warm provider does the distillation — never the slow big model on the full confused 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 MaxIterationsError ¶ added in v0.1.1
type MaxIterationsError struct {
Limit int
}
MaxIterationsError carries the cap so callers can surface it and so errors.Is(err, ErrMaxIterations) matches. Error() keeps the original guidance text (type 'continue' / /iterations) callers and tests rely on.
func (*MaxIterationsError) Error ¶ added in v0.1.1
func (e *MaxIterationsError) Error() string
func (*MaxIterationsError) Is ¶ added in v0.1.1
func (e *MaxIterationsError) Is(target error) bool
func (*MaxIterationsError) Unwrap ¶ added in v0.1.1
func (e *MaxIterationsError) Unwrap() error
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 ResumeDecision ¶ added in v0.1.1
type ResumeDecision struct {
Resume bool
Continuation string // synthetic user message to re-trigger with
Reason string // short label for the warn line / stderr
}
ResumeDecision is the policy output. Stateless — the caller owns the counter and the cap.
func ClassifyResume ¶ added in v0.1.1
func ClassifyResume(err error, res RunResult) ResumeDecision
ClassifyResume maps a finished Run (err + result) to a resume decision. The non-resumable cases are checked first so a clean finish, a user abort, or a deliberate escalate always wins over any resumable signal.
Resumable: a hang surfaced as context.DeadlineExceeded, a persistently dropped stream, a model wedge (after the loop's own in-turn nudges gave up), and hitting the iteration cap. Non-resumable: clean finish, user cancel, escalate, hard budget cap, and any unrecognised/fatal error (auth, provider down, config) — those should fail fast, not spin the resume budget.
type Role ¶ added in v0.1.1
type Role string
Role gates engine behavior per Run. one axis the user cycles with shift+tab; each role bundles tool surface + reasoning budget + orchestration.
RoleWorker: full tool surface; a per-turn classifier picks read-only vs act
so small models don't reflex into shell on a greeting. default.
RoleScout: read-only research + web; proposes a plan, never mutates.
RoleQueen: spawns a hive (decompose → workers → critic → synthesize).
yolo (auto-approve dangerous shell) is a separate toggle, not a role.
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
- handoff.go
- idempotency.go
- profile_tools.go
- recap.go
- repeat_detect.go
- repeat_stream.go
- resume.go
- role.go
- sandbox_wrap.go
- thinking_verify.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