loop

package
v0.9.1-rc.2 Latest Latest
Warning

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

Go to latest
Published: Apr 23, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package loop implements persistent goroutine-based delegate loops — lightweight autonomous observers that run continuously alongside the main agent, with direct completion delivery or ordinary tool use for any durable artifacts they need to maintain.

Loops are the universal primitive replacing previously-separate systems (metacognitive loop, observers) with a single Registry.SpawnLoop abstraction. Each loop is a background goroutine that iterates on a randomized bounded sleep schedule, running LLM iterations via the agent runner and optionally delivering results back through a completion path.

A Registry tracks all active loops and provides visibility into what is running, their health, and resource usage. It enforces concurrency limits and coordinates graceful shutdown.

See issue #509 for the full design.

Index

Constants

View Source
const (
	DefaultSleepMin       = 30 * time.Second
	DefaultSleepMax       = 5 * time.Minute
	DefaultSleepDefault   = 1 * time.Minute
	DefaultJitter         = 0.2
	DefaultSupervisorProb = 0.1
)

Default configuration values. Exported so callers can reference them when building Config values without memorizing magic numbers.

Variables

View Source
var ErrLoopStopped = errors.New("loop: cannot start a stopped loop")

ErrLoopStopped is returned by Loop.Start when the loop has already been stopped. A stopped loop cannot be restarted.

View Source
var ErrNilRunner = errors.New("loop: Runner is required")

ErrNilRunner is returned by New when Deps.Runner is nil.

View Source
var ErrNoOp = errors.New("loop: no-op iteration")

ErrNoOp is returned by a [Config.Handler] to signal that the iteration ran but produced no meaningful work (e.g., all received events were filtered out). When returned, the loop never transitions to StateProcessing, never publishes events.KindLoopIterationStart, and skips all post-iteration accounting (snapshot, attempt count, recentConvIDs) — so the dashboard activity indicator stays quiet. ErrNoOp is not counted as an error and does not increment [Status.ConsecutiveErrors].

Functions

func FallbackContent added in v0.9.1

func FallbackContent(ctx context.Context) string

FallbackContent returns the loop-configured response fallback from a handler context. Handler-backed interactive loops can pass this through to nested agent.Run calls and use it as a last-resort post-run reply.

func Float64Ptr

func Float64Ptr(v float64) *float64

Float64Ptr returns a pointer to v. Use it to set optional *float64 config fields like [Config.Jitter]:

Config{Jitter: loop.Float64Ptr(0.3)}   // custom jitter
Config{Jitter: loop.Float64Ptr(0)}     // explicitly no jitter
Config{}                                // nil → DefaultJitter

func IterationSummary

func IterationSummary(ctx context.Context) map[string]any

IterationSummary returns the summary map for the current iteration from the handler context, or nil if not available. Handler implementations call this to report per-iteration metrics to the dashboard timeline. Values should be small scalars (int, string, bool) to keep SSE payloads compact.

func LoopIDFromContext added in v0.8.4

func LoopIDFromContext(ctx context.Context) string

LoopIDFromContext extracts the originating loop ID from a handler context. Returns an empty string if the context was not created by a loop handler dispatch.

func NotifyEnvelopesFromContext added in v0.9.1

func NotifyEnvelopesFromContext(ctx context.Context) []messages.Envelope

NotifyEnvelopesFromContext returns one-shot message envelopes delivered to the current loop iteration, if any.

func ProgressFunc

func ProgressFunc(ctx context.Context) func(string, map[string]any)

ProgressFunc returns the loop's progress callback from the handler context, or nil if not available. Handler implementations that dispatch LLM calls can use this to forward in-flight events (tool calls, LLM responses) to the event bus for dashboard visibility.

The returned function has signature func(kind string, data map[string]any) where kind is an events.KindLoop* constant and data carries the event payload.

func ReportAgentRun added in v0.9.1

func ReportAgentRun(ctx context.Context, s AgentRunSummary) map[string]any

ReportAgentRun writes standard agent-run metrics into the current iteration's summary map. It is the canonical way for handler-only loops that call runner.Run() to surface request_id, model, and token counts on the dashboard.

Callers may add additional custom fields to the summary map after this call (e.g., sender, message_len).

Returns the summary map for chaining, or nil if the context has no iteration summary (e.g., called outside a loop handler).

func ReportConversationID added in v0.9.1

func ReportConversationID(ctx context.Context, conversationID string) map[string]any

ReportConversationID overrides the loop-visible conversation ID for the current handler iteration. Handler-only loops normally generate an internal conversation ID before dispatch; handlers that proxy a nested agent.Run can call this so the dashboard timeline and log lookups follow the real child conversation instead.

func SpawnDemoLoops

func SpawnDemoLoops(ctx context.Context, registry *Registry, eventBus *events.Bus, logger *slog.Logger) error

SpawnDemoLoops creates simulated loops covering all visual categories, parent/child hierarchies, error states, and node churn. Intended for dashboard layout iteration without real service dependencies.

Types

type AgentRunSummary added in v0.9.1

type AgentRunSummary struct {
	RequestID          string
	Model              string
	InputTokens        int
	OutputTokens       int
	ActiveTags         []string
	EffectiveTools     []string
	LoadedCapabilities []toolcatalog.LoadedCapabilityEntry
}

AgentRunSummary holds the subset of agent response fields that handler-only loops report to the dashboard timeline. It exists in the loop package (rather than accepting an agent.Response directly) to avoid an import cycle — agent imports loop, not the other way around.

type Completion added in v0.9.1

type Completion string

Completion describes how a loop's result should be delivered. The zero value is accepted while loops-ng adoption is incremental.

const (
	// CompletionReturn delivers the result directly to the caller.
	CompletionReturn Completion = "return"
	// CompletionConversation injects the result into a conversation.
	CompletionConversation Completion = "conversation"
	// CompletionChannel delivers the result to a channel integration.
	CompletionChannel Completion = "channel"
	// CompletionNone means the loop has no outward completion delivery.
	CompletionNone Completion = "none"
)

type CompletionChannelTarget added in v0.9.1

type CompletionChannelTarget struct {
	Channel        string `yaml:"channel,omitempty" json:"channel,omitempty"`
	Recipient      string `yaml:"recipient,omitempty" json:"recipient,omitempty"`
	ConversationID string `yaml:"conversation_id,omitempty" json:"conversation_id,omitempty"`
}

CompletionChannelTarget identifies a concrete interactive channel target for a detached loop completion. This keeps transport selection in the app layer while giving launches and deliveries an honest, structured contract for channel-shaped results.

func CloneCompletionChannelTarget added in v0.9.1

func CloneCompletionChannelTarget(t *CompletionChannelTarget) *CompletionChannelTarget

CloneCompletionChannelTarget returns a shallow copy of the target so callers can safely retain it without sharing mutable state.

func (*CompletionChannelTarget) Validate added in v0.9.1

func (t *CompletionChannelTarget) Validate() error

Validate checks that the channel target is well formed.

type CompletionDelivery added in v0.9.1

type CompletionDelivery struct {
	Mode           Completion               `yaml:"mode,omitempty" json:"mode"`
	ConversationID string                   `yaml:"conversation_id,omitempty" json:"conversation_id,omitempty"`
	Channel        *CompletionChannelTarget `yaml:"channel,omitempty" json:"channel,omitempty"`
	Content        string                   `yaml:"content,omitempty" json:"content"`
	LoopID         string                   `yaml:"loop_id,omitempty" json:"loop_id"`
	LoopName       string                   `yaml:"loop_name,omitempty" json:"loop_name"`
	Response       *Response                `yaml:"response,omitempty" json:"response,omitempty"`
	Status         *Status                  `yaml:"status,omitempty" json:"status,omitempty"`
}

CompletionDelivery is the normalized completion payload emitted by detached loops when they deliver a result through a non-return path.

type CompletionSink added in v0.9.1

type CompletionSink func(ctx context.Context, delivery CompletionDelivery) error

CompletionSink receives detached loop completion deliveries. The app layer wires concrete sinks (e.g. conversation injection) so the loop package can stay free of channel and memory dependencies.

type Conditions added in v0.9.1

type Conditions struct {
	// Schedule constrains when the definition is currently eligible for
	// runtime use. When unset, the definition is always eligible unless
	// blocked by policy.
	Schedule *ScheduleCondition `yaml:"schedule,omitempty" json:"schedule,omitempty"`
}

Conditions describes optional runtime eligibility gates for a loop definition. The first supported condition family is schedule-based, and additional condition families can be added here over time without changing the surrounding loops-ng contract shape.

func (Conditions) Evaluate added in v0.9.1

Evaluate returns the effective eligibility of the conditions at the provided time.

func (Conditions) Validate added in v0.9.1

func (c Conditions) Validate() error

Validate checks that all configured runtime conditions are structurally valid.

type Config

type Config struct {
	// Name is the unique identifier for this loop. Required.
	Name string

	// Task is the LLM prompt for each iteration. It describes what
	// the loop should observe, check, or do on each wake.
	Task string

	// Operation describes the runtime pattern expected for the loop.
	Operation Operation

	// Completion describes how results should be delivered back to a
	// caller, conversation, or channel.
	Completion Completion

	// Tags are capability tags for tool scoping. When non-empty,
	// the loop's tool registry is filtered to tools matching these
	// tags (plus always-active tags).
	Tags []string

	// ExcludeTools lists tool names to exclude from the loop's
	// available tools.
	ExcludeTools []string

	// SleepMin is the minimum sleep duration between iterations.
	// Default: 30s.
	SleepMin time.Duration

	// SleepMax is the maximum sleep duration between iterations.
	// Default: 5m.
	SleepMax time.Duration

	// SleepDefault is the initial sleep duration before the loop
	// self-adjusts. Default: 1m.
	SleepDefault time.Duration

	// Jitter is the randomization factor applied to sleep durations
	// to break periodicity. Range [0.0, 1.0]. Nil is defaulted to
	// DefaultJitter (0.2) by applyDefaults. Use Float64Ptr(0) to
	// explicitly disable jitter.
	Jitter *float64

	// MaxDuration is the maximum wall-clock time the loop may run.
	// Zero means unlimited.
	MaxDuration time.Duration

	// MaxIter is the maximum number of iteration attempts the loop
	// may make (including failures). Zero means unlimited.
	MaxIter int

	// Supervisor enables frontier model dice rolls. When true, a
	// fraction of iterations (controlled by SupervisorProb) use a
	// more capable model for oversight.
	Supervisor bool

	// SupervisorProb is the probability [0.0, 1.0] that a given
	// iteration uses the supervisor model. Only meaningful when
	// Supervisor is true. Zero means never (use DefaultSupervisorProb
	// for the recommended default).
	SupervisorProb float64

	// QualityFloor is the minimum model quality rating for normal
	// iterations. Zero uses the router default.
	QualityFloor int

	// SupervisorContext is an optional prompt prepended to the Task
	// during supervisor iterations. Use it to give the frontier model
	// review instructions, recent iteration summaries, or oversight
	// criteria. Empty means supervisor runs the same Task as normal.
	SupervisorContext string

	// SupervisorQualityFloor is the minimum model quality rating
	// for supervisor iterations. Zero uses the router default.
	SupervisorQualityFloor int

	// OnRetrigger determines behavior when the loop's start
	// condition fires again while running. Default: RetriggerSingle.
	OnRetrigger RetriggerMode

	// TaskBuilder is called per-iteration to generate a dynamic prompt.
	// When set, the static Task field is ignored. The isSupervisor
	// argument indicates whether this is a supervisor iteration.
	TaskBuilder func(ctx context.Context, isSupervisor bool) (string, error) `json:"-"`

	// PostIterate is called after each successful iteration. Use it
	// for side effects like appending iteration logs. Errors are
	// logged but do not count as iteration failures.
	PostIterate func(ctx context.Context, result IterationResult) error `json:"-"`

	// WaitFunc blocks until an external event arrives. When set, the
	// loop enters [StateWaiting] and calls WaitFunc instead of
	// sleeping between iterations. The returned value is passed to
	// Handler (if set) or discarded for LLM-based loops. If WaitFunc
	// returns a non-context error, the loop treats it as an iteration
	// error (backoff + retry).
	//
	// A (nil, nil) return is treated as a no-op wake: the loop skips
	// the processing phase entirely and re-enters the wait state
	// without counting an iteration. This means nil is a reserved
	// sentinel payload. Implementations that need to deliver a
	// meaningful event with no associated data should return a non-nil
	// sentinel (e.g. a zero-value struct) instead of nil.
	WaitFunc func(ctx context.Context) (any, error) `json:"-"`

	// Handler processes each iteration directly without an LLM call.
	// When set, [Deps].Runner is not required. Receives the event
	// from WaitFunc (nil for timer-triggered loops). Handler-only
	// loops still track iterations, errors, and health.
	//
	// Return [ErrNoOp] to signal that the iteration produced no
	// meaningful work (e.g., all events were filtered). The loop
	// skips iteration accounting and continues to the next cycle.
	Handler func(ctx context.Context, event any) error `json:"-"`

	// Hints are merged into Request hints for each iteration.
	// Config hints override loop-generated defaults (e.g., setting
	// "source" to "metacognitive" instead of "loop").
	Hints map[string]string

	// FallbackContent is static text used when the loop's nested agent run
	// or direct request/reply execution finishes without any user-visible
	// content. Interactive loops can set this to guarantee a reply.
	FallbackContent string

	// Setup is called by [Registry.SpawnLoop] after [New] but before
	// [Loop.Start]. Use it to register tools or perform other setup
	// that requires a *Loop reference before the goroutine launches.
	Setup func(l *Loop) `json:"-"`

	// Metadata holds arbitrary key/value pairs for the loop.
	Metadata map[string]string

	// ParentID is the loop ID of the parent that spawned this loop,
	// if any. Empty for top-level loops.
	ParentID string
}

Config holds the configuration for a loop. All fields with zero values use sensible defaults.

type DefinitionEligibilityStatus added in v0.9.1

type DefinitionEligibilityStatus struct {
	// Eligible reports whether the definition is currently eligible to
	// run or launch, before policy and operation-specific logic are
	// applied.
	Eligible bool `yaml:"eligible" json:"eligible"`
	// Reason describes why the definition is currently ineligible, when
	// known. Empty means either eligible or no explanatory detail.
	Reason string `yaml:"reason,omitempty" json:"reason,omitempty"`
	// NextTransitionAt is the next time the eligibility result is
	// expected to change, such as the next schedule boundary.
	NextTransitionAt time.Time `yaml:"next_transition_at,omitempty" json:"next_transition_at,omitempty"`
}

DefinitionEligibilityStatus is the effective runtime eligibility for one stored loop definition at a point in time.

type DefinitionPolicy added in v0.9.1

type DefinitionPolicy struct {
	State     DefinitionPolicyState `yaml:"state,omitempty" json:"state,omitempty"`
	Reason    string                `yaml:"reason,omitempty" json:"reason,omitempty"`
	UpdatedAt time.Time             `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
}

DefinitionPolicy is the mutable runtime policy overlay for one loop definition.

type DefinitionPolicySource added in v0.9.1

type DefinitionPolicySource string

DefinitionPolicySource reports whether the effective runtime state comes from the definition's default baseline or an explicit overlay.

const (
	// DefinitionPolicySourceDefault means the effective state is derived
	// from the definition's own baseline fields.
	DefinitionPolicySourceDefault DefinitionPolicySource = "default"
	// DefinitionPolicySourceOverlay means the effective state is driven
	// by a persisted runtime override.
	DefinitionPolicySourceOverlay DefinitionPolicySource = "overlay"
)

type DefinitionPolicyState added in v0.9.1

type DefinitionPolicyState string

DefinitionPolicyState describes the effective runtime state of a stored loop definition.

const (
	// DefinitionPolicyStateActive means the definition is eligible for
	// runtime use. Service definitions may auto-start or be launched.
	DefinitionPolicyStateActive DefinitionPolicyState = "active"
	// DefinitionPolicyStatePaused means the definition is temporarily
	// held out of runtime execution. Existing service loops should be
	// stopped, but the definition remains retained for future resume.
	DefinitionPolicyStatePaused DefinitionPolicyState = "paused"
	// DefinitionPolicyStateInactive means the definition is disabled for
	// runtime use. Existing service loops should be stopped.
	DefinitionPolicyStateInactive DefinitionPolicyState = "inactive"
)

func ParseDefinitionPolicyState added in v0.9.1

func ParseDefinitionPolicyState(raw string) (DefinitionPolicyState, error)

ParseDefinitionPolicyState validates a caller-provided definition policy state.

type DefinitionRecord added in v0.9.1

type DefinitionRecord struct {
	Spec      Spec      `yaml:"spec,omitempty" json:"spec,omitempty"`
	UpdatedAt time.Time `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
}

DefinitionRecord is one persistable loop definition plus its update timestamp. It is the durable unit stored in the dynamic overlay.

type DefinitionRegistry added in v0.9.1

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

DefinitionRegistry holds the immutable config-defined loop definitions plus a mutable persistent overlay for dynamically created definitions. It does not track active loop runs; that remains Registry's job.

func NewDefinitionRegistry added in v0.9.1

func NewDefinitionRegistry(base []Spec) (*DefinitionRegistry, error)

NewDefinitionRegistry constructs a loop-definition registry from the immutable config-defined base definitions.

func (*DefinitionRegistry) ApplyPolicy added in v0.9.1

func (r *DefinitionRegistry) ApplyPolicy(name string, policy DefinitionPolicy, updatedAt time.Time) error

ApplyPolicy upserts a runtime policy override for one stored loop definition.

func (*DefinitionRegistry) ClearPolicy added in v0.9.1

func (r *DefinitionRegistry) ClearPolicy(name string, updatedAt time.Time) error

ClearPolicy removes an explicit runtime policy override for one loop definition and returns it to its default state.

func (*DefinitionRegistry) Delete added in v0.9.1

func (r *DefinitionRegistry) Delete(name string, updatedAt time.Time) error

Delete removes one dynamic loop definition from the overlay.

func (*DefinitionRegistry) Get added in v0.9.1

func (r *DefinitionRegistry) Get(name string) (Spec, bool)

Get returns the effective definition with the given name.

func (*DefinitionRegistry) ReplaceOverlay added in v0.9.1

func (r *DefinitionRegistry) ReplaceOverlay(records map[string]DefinitionRecord) error

ReplaceOverlay replaces the entire dynamic overlay. It is intended for startup-time hydration from persistent state.

func (*DefinitionRegistry) ReplacePolicies added in v0.9.1

func (r *DefinitionRegistry) ReplacePolicies(policies map[string]DefinitionPolicy, updatedAt time.Time) error

ReplacePolicies swaps the explicit runtime policy overlay with the provided policy map during startup-time hydration.

func (*DefinitionRegistry) Snapshot added in v0.9.1

Snapshot returns a read-only snapshot of the effective loop definitions, sorted by name.

func (*DefinitionRegistry) Upsert added in v0.9.1

func (r *DefinitionRegistry) Upsert(spec Spec, updatedAt time.Time) error

Upsert stores or replaces one dynamic loop definition in the overlay.

type DefinitionRegistrySnapshot added in v0.9.1

type DefinitionRegistrySnapshot struct {
	Generation         int64                `yaml:"generation,omitempty" json:"generation"`
	UpdatedAt          time.Time            `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
	ConfigDefinitions  int                  `yaml:"config_definitions,omitempty" json:"config_definitions"`
	OverlayDefinitions int                  `yaml:"overlay_definitions,omitempty" json:"overlay_definitions"`
	Definitions        []DefinitionSnapshot `yaml:"definitions,omitempty" json:"definitions,omitempty"`
}

DefinitionRegistrySnapshot is a read-only snapshot of the effective loop definition registry.

type DefinitionRegistryView added in v0.9.1

type DefinitionRegistryView struct {
	Generation              int64            `yaml:"generation,omitempty" json:"generation"`
	UpdatedAt               time.Time        `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
	ConfigDefinitions       int              `yaml:"config_definitions,omitempty" json:"config_definitions"`
	OverlayDefinitions      int              `yaml:"overlay_definitions,omitempty" json:"overlay_definitions"`
	RunningDefinitions      int              `yaml:"running_definitions,omitempty" json:"running_definitions"`
	DefinitionsWithWarnings int              `yaml:"definitions_with_warnings,omitempty" json:"definitions_with_warnings,omitempty"`
	WarningCount            int              `yaml:"warning_count,omitempty" json:"warning_count,omitempty"`
	ByPolicyState           map[string]int   `yaml:"by_policy_state,omitempty" json:"by_policy_state,omitempty"`
	ByEligibilityState      map[string]int   `yaml:"by_eligibility_state,omitempty" json:"by_eligibility_state,omitempty"`
	ByRuntimeState          map[string]int   `yaml:"by_runtime_state,omitempty" json:"by_runtime_state,omitempty"`
	Definitions             []DefinitionView `yaml:"definitions,omitempty" json:"definitions,omitempty"`
}

DefinitionRegistryView is the effective combined view of stored loop definitions plus their current live runtime state.

func BuildDefinitionRegistryView added in v0.9.1

func BuildDefinitionRegistryView(snapshot *DefinitionRegistrySnapshot, runtime map[string]DefinitionRuntimeStatus) *DefinitionRegistryView

BuildDefinitionRegistryView combines the durable definition snapshot with an optional runtime-state map to produce the effective loops-ng registry view used by API and tool read surfaces.

type DefinitionRuntimeStatus added in v0.9.1

type DefinitionRuntimeStatus struct {
	// Running reports whether a live loop instance currently exists for
	// this definition.
	Running bool `yaml:"running" json:"running"`
	// LoopID is the backing live loop instance, when one is present.
	LoopID string `yaml:"loop_id,omitempty" json:"loop_id,omitempty"`
	// State is the current runtime lifecycle state of the backing loop.
	State State `yaml:"state,omitempty" json:"state,omitempty"`
	// StartedAt is when the current backing loop instance started.
	StartedAt time.Time `yaml:"started_at,omitempty" json:"started_at,omitempty"`
	// LastWakeAt is when the current backing loop most recently began an
	// iteration.
	LastWakeAt time.Time `yaml:"last_wake_at,omitempty" json:"last_wake_at,omitempty"`
	// Iterations is the number of successful iterations completed by the
	// current backing loop instance.
	Iterations int `yaml:"iterations,omitempty" json:"iterations,omitempty"`
	// Attempts is the number of total iteration attempts completed by the
	// current backing loop instance.
	Attempts int `yaml:"attempts,omitempty" json:"attempts,omitempty"`
	// LastError is the most recent runtime error from the current backing
	// loop instance.
	LastError string `yaml:"last_error,omitempty" json:"last_error,omitempty"`
}

DefinitionRuntimeStatus summarizes the live runtime state currently associated with one stored loop definition.

type DefinitionSnapshot added in v0.9.1

type DefinitionSnapshot struct {
	Name            string                 `yaml:"name,omitempty" json:"name"`
	Source          DefinitionSource       `yaml:"source,omitempty" json:"source"`
	UpdatedAt       time.Time              `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
	PolicyState     DefinitionPolicyState  `yaml:"policy_state,omitempty" json:"policy_state,omitempty"`
	PolicySource    DefinitionPolicySource `yaml:"policy_source,omitempty" json:"policy_source,omitempty"`
	PolicyReason    string                 `yaml:"policy_reason,omitempty" json:"policy_reason,omitempty"`
	PolicyUpdatedAt time.Time              `yaml:"policy_updated_at,omitempty" json:"policy_updated_at,omitempty"`
	Spec            Spec                   `yaml:"spec,omitempty" json:"spec,omitempty"`
}

DefinitionSnapshot is the API-facing state for one effective loop definition.

type DefinitionSource added in v0.9.1

type DefinitionSource string

DefinitionSource identifies where a loop definition came from.

const (
	// DefinitionSourceConfig means the definition came from immutable
	// config-file input.
	DefinitionSourceConfig DefinitionSource = "config"
	// DefinitionSourceOverlay means the definition came from the mutable
	// persistent overlay.
	DefinitionSourceOverlay DefinitionSource = "overlay"
)

type DefinitionView added in v0.9.1

type DefinitionView struct {
	DefinitionSnapshot `yaml:",inline"`
	Eligibility        DefinitionEligibilityStatus `yaml:"eligibility,omitempty" json:"eligibility"`
	Runtime            DefinitionRuntimeStatus     `yaml:"runtime,omitempty" json:"runtime"`
	Warnings           []DefinitionWarning         `yaml:"warnings,omitempty" json:"warnings,omitempty"`
}

DefinitionView is the combined stored-definition and live-runtime view exposed by loops-ng read surfaces.

type DefinitionWarning added in v0.9.1

type DefinitionWarning struct {
	Code    string `yaml:"code" json:"code"`
	Message string `yaml:"message" json:"message"`
}

DefinitionWarning is a non-fatal authoring concern discovered while inspecting a persistable loops-ng definition. Warnings are surfaced through definition views and lint tooling so the model can correct likely mistakes before they become noisy runtime behavior.

func BuildDefinitionWarnings added in v0.9.1

func BuildDefinitionWarnings(spec Spec) []DefinitionWarning

BuildDefinitionWarnings returns authoring warnings for one loop spec. These warnings are advisory: the spec may still validate and persist.

type Deps

type Deps struct {
	// Runner executes LLM iterations. Required unless [Config].Handler
	// is set (Handler-only loops do not call the Runner).
	Runner Runner
	// CompletionSink receives detached completion deliveries such as
	// background-task results injected into conversations.
	CompletionSink CompletionSink
	// Logger for loop operations. Defaults to slog.Default().
	Logger *slog.Logger
	// EventBus publishes loop lifecycle events. Nil disables events.
	EventBus *events.Bus
	// Rand provides randomness for sleep jitter and supervisor dice.
	// Nil uses math/rand/v2 default.
	Rand RandSource
}

Deps holds injected dependencies for a loop. Using a struct avoids a growing parameter list as loops evolve.

type ImmutableDefinitionError added in v0.9.1

type ImmutableDefinitionError struct {
	Name string
}

ImmutableDefinitionError reports an attempted mutation of an immutable config-defined loop definition.

func (*ImmutableDefinitionError) Error added in v0.9.1

func (e *ImmutableDefinitionError) Error() string

type InactiveDefinitionError added in v0.9.1

type InactiveDefinitionError struct {
	Name string
}

InactiveDefinitionError reports that a loop definition exists but is currently disabled by effective runtime policy.

func (*InactiveDefinitionError) Error added in v0.9.1

func (e *InactiveDefinitionError) Error() string

type IneligibleDefinitionError added in v0.9.1

type IneligibleDefinitionError struct {
	Name   string
	Reason string
}

IneligibleDefinitionError reports that a loop definition exists and is active by policy, but its runtime conditions do not currently permit launch.

func (*IneligibleDefinitionError) Error added in v0.9.1

func (e *IneligibleDefinitionError) Error() string

type IterationResult

type IterationResult struct {
	// ConvID is the conversation ID for this iteration.
	ConvID string
	// RequestID is the agent-generated request ID for this iteration.
	RequestID string
	// Model is the LLM model used for this iteration.
	Model string
	// InputTokens is the number of input tokens consumed.
	InputTokens int
	// OutputTokens is the number of output tokens produced.
	OutputTokens int
	// ContextWindow is the maximum context size (in tokens) of the model used.
	ContextWindow int
	// ToolsUsed maps tool names to invocation counts.
	ToolsUsed map[string]int
	// EffectiveTools lists the tools that were visible to the model for
	// this iteration after allowlists, excludes, capability tags, and
	// delegation gating were applied.
	EffectiveTools []string
	// ActiveTags holds the capability tags active at the end of this
	// iteration.
	ActiveTags []string
	// LoadedCapabilities captures the structured capability entries
	// corresponding to the tags loaded for this iteration.
	LoadedCapabilities []toolcatalog.LoadedCapabilityEntry
	// Elapsed is the wall-clock duration of the iteration.
	Elapsed time.Duration
	// Supervisor indicates whether this was a supervisor iteration.
	Supervisor bool
	// Sleep is the computed sleep duration before the next iteration.
	Sleep time.Duration
}

IterationResult holds data from a completed loop iteration, passed to [Config.PostIterate] callbacks.

type IterationSnapshot

type IterationSnapshot struct {
	// Number is the 1-based iteration number (matches the loop's
	// cumulative iteration counter at the time of completion).
	Number int `json:"number"`
	// ConvID is the conversation ID used for this iteration.
	ConvID string `json:"conv_id,omitempty"`
	// RequestID is the agent-generated request ID, linking to
	// log_request_content for prompt/response inspection.
	RequestID string `json:"request_id,omitempty"`
	// Model is the LLM model used.
	Model string `json:"model,omitempty"`
	// InputTokens consumed by this iteration.
	InputTokens int `json:"input_tokens,omitempty"`
	// OutputTokens produced by this iteration.
	OutputTokens int `json:"output_tokens,omitempty"`
	// ContextWindow is the model's maximum context size in tokens.
	ContextWindow int `json:"context_window,omitempty"`
	// ToolsUsed maps tool names to invocation counts.
	ToolsUsed map[string]int `json:"tools_used,omitempty"`
	// EffectiveTools lists the tools visible to the model for this turn.
	EffectiveTools []string `json:"effective_tools,omitempty"`
	// ActiveTags holds the capability tags active for this turn.
	ActiveTags []string `json:"active_tags,omitempty"`
	// Tooling captures the authoritative tool/capability view for this turn.
	Tooling ToolingState `json:"tooling,omitempty"`
	// ElapsedMs is the wall-clock duration of the iteration in
	// milliseconds. Stored as int64 (not time.Duration) so the JSON
	// value is directly usable by the client without nanosecond
	// conversion.
	ElapsedMs int64 `json:"elapsed_ms"`
	// Supervisor indicates whether this was a supervisor iteration.
	Supervisor bool `json:"supervisor,omitempty"`
	// Error holds the error message if the iteration failed.
	Error string `json:"error,omitempty"`
	// StartedAt is when the iteration began.
	StartedAt time.Time `json:"started_at"`
	// CompletedAt is when the iteration finished.
	CompletedAt time.Time `json:"completed_at"`
	// SleepAfterMs is the computed sleep duration (in milliseconds)
	// following this iteration. Zero for WaitFunc-based loops.
	SleepAfterMs int64 `json:"sleep_after_ms,omitempty"`
	// WaitAfter is true when the loop entered WaitFunc after this
	// iteration instead of sleeping.
	WaitAfter bool `json:"wait_after,omitempty"`
	// Summary holds handler-reported metrics for this iteration,
	// written by handlers via [IterationSummary] during execution.
	// Values should be small scalars (int, string, bool).
	Summary map[string]any `json:"summary,omitempty"`
}

IterationSnapshot is a serializable summary of a completed loop iteration, retained in a ring buffer for the dashboard timeline.

type Launch added in v0.9.1

type Launch struct {
	Spec           Spec                                   `yaml:"spec,omitempty" json:"spec,omitempty"`
	Task           string                                 `yaml:"task,omitempty" json:"task,omitempty"`
	ParentID       string                                 `yaml:"parent_id,omitempty" json:"parent_id,omitempty"`
	Metadata       map[string]string                      `yaml:"metadata,omitempty" json:"metadata,omitempty"`
	ConversationID string                                 `yaml:"conversation_id,omitempty" json:"conversation_id,omitempty"`
	ChannelBinding *memory.ChannelBinding                 `yaml:"channel_binding,omitempty" json:"channel_binding,omitempty"`
	Model          string                                 `yaml:"model,omitempty" json:"model,omitempty"`
	Hints          map[string]string                      `yaml:"hints,omitempty" json:"hints,omitempty"`
	AllowedTools   []string                               `yaml:"allowed_tools,omitempty" json:"allowed_tools,omitempty"`
	ExcludeTools   []string                               `yaml:"exclude_tools,omitempty" json:"exclude_tools,omitempty"`
	InitialTags    []string                               `yaml:"initial_tags,omitempty" json:"initial_tags,omitempty"`
	OnProgress     func(kind string, data map[string]any) `yaml:"-" json:"-"`
	RunTimeout     time.Duration                          `yaml:"run_timeout,omitempty" json:"run_timeout,omitempty"`
	// CompletionConversationID names the live conversation that should
	// receive detached completion delivery when Spec.Completion is
	// CompletionConversation.
	CompletionConversationID string `yaml:"completion_conversation_id,omitempty" json:"completion_conversation_id,omitempty"`
	// CompletionChannel identifies the interactive channel target that
	// should receive detached completion delivery when Spec.Completion
	// is CompletionChannel.
	CompletionChannel *CompletionChannelTarget `yaml:"completion_channel,omitempty" json:"completion_channel,omitempty"`
	SkipContext       bool                     `yaml:"skip_context,omitempty" json:"skip_context,omitempty"`
	SkipTagFilter     bool                     `yaml:"skip_tag_filter,omitempty" json:"skip_tag_filter,omitempty"`
	SystemPrompt      string                   `yaml:"system_prompt,omitempty" json:"system_prompt,omitempty"`
	FallbackContent   string                   `yaml:"fallback_content,omitempty" json:"fallback_content,omitempty"`
	MaxIterations     int                      `yaml:"max_iterations,omitempty" json:"max_iterations,omitempty"`
	MaxOutputTokens   int                      `yaml:"max_output_tokens,omitempty" json:"max_output_tokens,omitempty"`
	ToolTimeout       time.Duration            `yaml:"tool_timeout,omitempty" json:"tool_timeout,omitempty"`
	UsageRole         string                   `yaml:"usage_role,omitempty" json:"usage_role,omitempty"`
	UsageTaskName     string                   `yaml:"usage_task_name,omitempty" json:"usage_task_name,omitempty"`
}

Launch describes a single loops-ng launch request. It is separate from Spec so per-launch overrides and delivery hooks can grow here over time without turning Spec itself into an ephemeral run object.

func (Launch) MarshalJSON added in v0.9.1

func (l Launch) MarshalJSON() ([]byte, error)

func (*Launch) UnmarshalJSON added in v0.9.1

func (l *Launch) UnmarshalJSON(data []byte) error

func (*Launch) Validate added in v0.9.1

func (l *Launch) Validate() error

Validate checks that the launch is well-formed.

type LaunchResult added in v0.9.1

type LaunchResult struct {
	LoopID      string    `yaml:"loop_id,omitempty" json:"loop_id"`
	Operation   Operation `yaml:"operation,omitempty" json:"operation"`
	Detached    bool      `yaml:"detached,omitempty" json:"detached"`
	Response    *Response `yaml:"response,omitempty" json:"response,omitempty"`
	FinalStatus *Status   `yaml:"final_status,omitempty" json:"final_status,omitempty"`
}

LaunchResult is the outcome of starting a loop via Registry.Launch. Request/reply launches wait for completion and return a final status; detached launches return immediately with the new loop ID.

type Loop

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

Loop is a persistent background goroutine that iterates on a timer or in response to external events. Each iteration runs an LLM call via the agent runner, or a direct [Config.Handler] function for infrastructure loops that don't need an LLM. Create with New, start with [Start], stop with [Stop].

func New

func New(cfg Config, deps Deps) (*Loop, error)

New creates a loop with the given configuration and dependencies. Returns an error if required fields are missing or invalid. Call Loop.Start to launch the background goroutine.

func NewFromLaunch added in v0.9.1

func NewFromLaunch(launch Launch, deps Deps) (*Loop, error)

NewFromLaunch creates a loop from a Launch, validating the launch, compiling the underlying Spec, and applying per-run request and metadata overrides. This is the additive bridge used by Registry.Launch.

func NewFromSpec added in v0.9.1

func NewFromSpec(spec Spec, deps Deps) (*Loop, error)

NewFromSpec creates a loop from a Spec, validating the loops-ng fields before compiling the engine-facing Config. This is an additive bridge for gradually moving call sites onto Spec.

func (*Loop) CurrentConvID

func (l *Loop) CurrentConvID() string

CurrentConvID returns the conversation ID of the in-flight iteration, or empty string if no iteration is running. Tool handlers use this to tag their outputs with the current conversation.

func (*Loop) Done

func (l *Loop) Done() <-chan struct{}

Done returns a channel that is closed when the loop's goroutine exits. Returns nil if the loop has not been started.

func (*Loop) ID

func (l *Loop) ID() string

ID returns the unique loop identifier.

func (*Loop) Name

func (l *Loop) Name() string

Name returns the loop's configured name.

func (*Loop) SetActiveTagsFunc added in v0.9.1

func (l *Loop) SetActiveTagsFunc(fn func() []string)

SetActiveTagsFunc configures an optional callback that returns the currently active capability tags. When set, Status includes the result so the dashboard can display dynamically activated capabilities.

func (*Loop) SetNextSleep

func (l *Loop) SetNextSleep(d time.Duration)

SetNextSleep sets the sleep duration for the next cycle. This is intended for tool handlers (e.g., set_next_sleep) to communicate the LLM's chosen sleep duration back to the loop.

func (*Loop) Start

func (l *Loop) Start(ctx context.Context) error

Start launches the background goroutine. Calling Start on an already running loop is a no-op (returns nil). Returns ErrLoopStopped if Loop.Stop was called before Start. The goroutine runs until ctx is cancelled or Loop.Stop is called.

func (*Loop) Status

func (l *Loop) Status() Status

Status returns a snapshot of the loop's current state and metrics. The returned Config is a deep copy; callers cannot mutate loop state via the snapshot.

func (*Loop) Stop

func (l *Loop) Stop()

Stop cancels the loop and waits for the goroutine to exit. Safe to call multiple times or before Start. After Stop, Loop.Start will return ErrLoopStopped. Blocks until the goroutine exits or 10 seconds elapse.

type Message added in v0.9.1

type Message struct {
	Role    string `yaml:"role" json:"role"`
	Content string `yaml:"content" json:"content"`
}

Message is a chat message for the runner.

type NotifyReceipt added in v0.9.1

type NotifyReceipt struct {
	LoopID               string `json:"loop_id"`
	LoopName             string `json:"loop_name"`
	State                State  `json:"state"`
	WokeImmediately      bool   `json:"woke_immediately,omitempty"`
	QueuedForNextWake    bool   `json:"queued_for_next_wake,omitempty"`
	ForceSupervisor      bool   `json:"force_supervisor,omitempty"`
	PendingNotifications int    `json:"pending_notifications,omitempty"`
}

NotifyReceipt summarizes the effect of notifying a live loop.

type Operation added in v0.9.1

type Operation string

Operation describes the runtime pattern a loop is expected to follow. The zero value is accepted while loops-ng adoption is incremental.

const (
	// OperationRequestReply is a one-shot run that is expected to
	// conclude with a direct result for the caller.
	OperationRequestReply Operation = "request_reply"
	// OperationBackgroundTask is a detached task whose result is
	// delivered later through a non-blocking completion path.
	OperationBackgroundTask Operation = "background_task"
	// OperationService is a persistent loop such as metacognition,
	// ego, or a long-running watcher.
	OperationService Operation = "service"
)

type PausedDefinitionError added in v0.9.1

type PausedDefinitionError struct {
	Name string
}

PausedDefinitionError reports that a loop definition exists but is currently paused by effective runtime policy.

func (*PausedDefinitionError) Error added in v0.9.1

func (e *PausedDefinitionError) Error() string

type RandSource

type RandSource interface {
	Float64() float64
}

RandSource abstracts randomness for deterministic testing.

type Registry

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

Registry tracks all active loops and provides visibility into what is running. It enforces concurrency limits and coordinates graceful shutdown.

func NewRegistry

func NewRegistry(opts ...RegistryOption) *Registry

NewRegistry creates a new loop registry.

func (*Registry) ActiveCount

func (r *Registry) ActiveCount() int

ActiveCount returns the number of registered loops.

func (*Registry) Deregister

func (r *Registry) Deregister(id string)

Deregister removes a loop from the registry. Safe to call for a loop that is not registered (no-op).

func (*Registry) FindByName added in v0.9.1

func (r *Registry) FindByName(name string) []*Loop

FindByName returns all live loops with the exact given name.

func (*Registry) Get

func (r *Registry) Get(id string) *Loop

Get returns the loop with the given ID, or nil if not found.

func (*Registry) GetByName

func (r *Registry) GetByName(name string) *Loop

GetByName returns the first loop with the given name, or nil if not found. If multiple loops share a name, the result is undefined.

func (*Registry) Launch added in v0.9.1

func (r *Registry) Launch(ctx context.Context, launch Launch, deps Deps) (LaunchResult, error)

Launch starts a loops-ng Launch. Request/reply launches wait for completion and return a final status snapshot; background and service launches detach immediately and leave the loop running in the registry.

func (*Registry) List

func (r *Registry) List() []*Loop

List returns a snapshot of all registered loops sorted by name.

func (*Registry) MaxLoops added in v0.9.1

func (r *Registry) MaxLoops() int

MaxLoops returns the configured concurrency limit. Zero means unlimited.

func (*Registry) NotifyLoop added in v0.9.1

func (r *Registry) NotifyLoop(ctx context.Context, id string, env messages.Envelope) (NotifyReceipt, error)

NotifyLoop delivers one inter-loop process notification to a live loop by ID. This is loop-runtime control messaging, not Signal-channel transport.

func (*Registry) NotifyLoopByName added in v0.9.1

func (r *Registry) NotifyLoopByName(ctx context.Context, name string, env messages.Envelope) (NotifyReceipt, error)

NotifyLoopByName delivers one inter-loop process notification to a live loop by exact name. This is loop-runtime control messaging, not Signal-channel transport.

func (*Registry) Register

func (r *Registry) Register(l *Loop) error

Register adds a loop to the registry. Returns an error if the loop's ID is already registered or the concurrency limit would be exceeded. The loop is not started — call Loop.Start after registering.

func (*Registry) ShutdownAll

func (r *Registry) ShutdownAll(ctx context.Context) int

ShutdownAll cancels all registered loops and waits for them to drain. The provided context controls the maximum time to wait; if it expires, remaining loops are abandoned. Returns the number of loops that were stopped.

func (*Registry) SpawnLoop

func (r *Registry) SpawnLoop(ctx context.Context, cfg Config, deps Deps) (string, error)

SpawnLoop creates a new loop with the given config, registers it, and starts it. This is the primary entry point for creating loops. Returns the loop ID on success.

func (*Registry) SpawnSpec added in v0.9.1

func (r *Registry) SpawnSpec(ctx context.Context, spec Spec, deps Deps) (string, error)

SpawnSpec creates, registers, and starts a loop from a Spec. It is the additive loops-ng entrypoint while existing call sites continue to use Registry.SpawnLoop with Config.

func (*Registry) Statuses

func (r *Registry) Statuses() []Status

Statuses returns a snapshot of all registered loop statuses sorted by name.

func (*Registry) StopLoop

func (r *Registry) StopLoop(id string) error

StopLoop stops a loop by ID and deregisters it once the goroutine has exited. Returns an error if the loop is not found. If the goroutine does not exit within 10 seconds (the Stop timeout), the loop remains registered to avoid orphaning a running goroutine.

func (*Registry) StopLoopByName added in v0.9.1

func (r *Registry) StopLoopByName(name string) error

StopLoopByName cancels one registered loop by exact name. It returns an error when no loop matches or when the name is ambiguous.

type RegistryOption

type RegistryOption func(*Registry)

RegistryOption configures a Registry.

func WithMaxLoops

func WithMaxLoops(n int) RegistryOption

WithMaxLoops sets the maximum number of concurrent loops the registry will allow. Zero means unlimited.

func WithRegistryLogger

func WithRegistryLogger(l *slog.Logger) RegistryOption

WithRegistryLogger sets the logger for registry operations. Nil is ignored (keeps slog.Default()).

type Request added in v0.9.1

type Request struct {
	Model          string                 `yaml:"model,omitempty" json:"model,omitempty"`
	ConversationID string                 `yaml:"conversation_id,omitempty" json:"conversation_id,omitempty"`
	ChannelBinding *memory.ChannelBinding `yaml:"channel_binding,omitempty" json:"channel_binding,omitempty"`
	Messages       []Message              `yaml:"messages,omitempty" json:"messages,omitempty"`
	SkipContext    bool                   `yaml:"skip_context,omitempty" json:"skip_context,omitempty"`
	AllowedTools   []string               `yaml:"allowed_tools,omitempty" json:"allowed_tools,omitempty"`
	ExcludeTools   []string               `yaml:"exclude_tools,omitempty" json:"exclude_tools,omitempty"`
	SkipTagFilter  bool                   `yaml:"skip_tag_filter,omitempty" json:"skip_tag_filter,omitempty"`
	Hints          map[string]string      `yaml:"hints,omitempty" json:"hints,omitempty"`
	// InitialTags are capability tags to activate at the start of the Run,
	// in addition to always-active and channel-pinned tags. Used by loops
	// to carry forward tags activated in previous iterations.
	InitialTags []string `yaml:"initial_tags,omitempty" json:"initial_tags,omitempty"`

	// OnProgress is called by the Runner during execution to report
	// in-flight activity (tool calls, LLM responses). The kind
	// parameter maps to an [events.Kind] constant; data holds
	// event-specific fields. The loop automatically injects loop_id
	// and loop_name into data before publishing. Nil means no
	// progress reporting.
	OnProgress func(kind string, data map[string]any) `yaml:"-" json:"-"`

	MaxIterations   int           `yaml:"max_iterations,omitempty" json:"max_iterations,omitempty"`
	MaxOutputTokens int           `yaml:"max_output_tokens,omitempty" json:"max_output_tokens,omitempty"`
	ToolTimeout     time.Duration `yaml:"tool_timeout,omitempty" json:"tool_timeout,omitempty"`
	UsageRole       string        `yaml:"usage_role,omitempty" json:"usage_role,omitempty"`
	UsageTaskName   string        `yaml:"usage_task_name,omitempty" json:"usage_task_name,omitempty"`
	SystemPrompt    string        `yaml:"system_prompt,omitempty" json:"system_prompt,omitempty"`
	FallbackContent string        `yaml:"fallback_content,omitempty" json:"fallback_content,omitempty"`
}

Request mirrors the loop-facing fields of agent.Request. The loop package defines its own type to avoid importing agent.

type Response added in v0.9.1

type Response struct {
	Content                  string                              `yaml:"content,omitempty" json:"content,omitempty"`
	Model                    string                              `yaml:"model,omitempty" json:"model,omitempty"`
	FinishReason             string                              `yaml:"finish_reason,omitempty" json:"finish_reason,omitempty"`
	InputTokens              int                                 `yaml:"input_tokens,omitempty" json:"input_tokens,omitempty"`
	OutputTokens             int                                 `yaml:"output_tokens,omitempty" json:"output_tokens,omitempty"`
	CacheCreationInputTokens int                                 `yaml:"cache_creation_input_tokens,omitempty" json:"cache_creation_input_tokens,omitempty"`
	CacheReadInputTokens     int                                 `yaml:"cache_read_input_tokens,omitempty" json:"cache_read_input_tokens,omitempty"`
	ContextWindow            int                                 `yaml:"context_window,omitempty" json:"context_window,omitempty"`
	ToolsUsed                map[string]int                      `yaml:"tools_used,omitempty" json:"tools_used,omitempty"`
	EffectiveTools           []string                            `yaml:"effective_tools,omitempty" json:"effective_tools,omitempty"`
	LoadedCapabilities       []toolcatalog.LoadedCapabilityEntry `yaml:"loaded_capabilities,omitempty" json:"loaded_capabilities,omitempty"`
	RequestID                string                              `yaml:"request_id,omitempty" json:"request_id,omitempty"`
	Iterations               int                                 `yaml:"iterations,omitempty" json:"iterations,omitempty"`
	Exhausted                bool                                `yaml:"exhausted,omitempty" json:"exhausted,omitempty"`
	// ActiveTags is the set of capability tags that were active at the
	// end of the Run. Loops use this to carry forward activations to
	// subsequent iterations.
	ActiveTags []string `yaml:"active_tags,omitempty" json:"active_tags,omitempty"`
}

Response mirrors agent.Response fields that loops consume. It holds the result of an LLM call executed by a Runner.

type RetriggerMode

type RetriggerMode int

RetriggerMode determines what happens when a loop's start condition fires again while the loop is already running.

const (
	// RetriggerSingle ignores re-triggers while the loop is running.
	RetriggerSingle RetriggerMode = iota
	// RetriggerRestart cancels the current loop and starts fresh.
	RetriggerRestart
	// RetriggerQueue queues the trigger and runs after current completes.
	RetriggerQueue
	// RetriggerSpawn spawns another instance of the loop.
	RetriggerSpawn
)

func ParseRetriggerMode added in v0.9.1

func ParseRetriggerMode(raw string) (RetriggerMode, error)

ParseRetriggerMode parses the stable textual form of a retrigger mode.

func (RetriggerMode) MarshalText added in v0.9.1

func (m RetriggerMode) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (RetriggerMode) String added in v0.9.1

func (m RetriggerMode) String() string

String returns the stable textual form of the retrigger mode.

func (*RetriggerMode) UnmarshalText added in v0.9.1

func (m *RetriggerMode) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

type RunMessage

type RunMessage = Message

RunMessage is kept as a compatibility alias while loops-ng migrates onto Message as the primary loop-facing message type.

type RunRequest

type RunRequest = Request

RunRequest is kept as a compatibility alias while loops-ng migrates onto Request as the primary loop-facing run descriptor.

type RunResponse

type RunResponse = Response

RunResponse is kept as a compatibility alias while loops-ng migrates onto Response as the primary loop-facing response type.

type Runner

type Runner interface {
	Run(ctx context.Context, req Request, stream StreamCallback) (*Response, error)
}

Runner abstracts the agent loop for LLM calls. Satisfied by *agent.Loop. Defined here to avoid a circular import.

type ScheduleCondition added in v0.9.1

type ScheduleCondition struct {
	// Timezone is the IANA timezone used to interpret window start and
	// end times. Empty means the local system timezone.
	Timezone string `yaml:"timezone,omitempty" json:"timezone,omitempty"`
	// Windows are the recurring day/time windows during which the
	// definition is eligible to run or launch.
	Windows []ScheduleWindow `yaml:"windows,omitempty" json:"windows,omitempty"`
}

ScheduleCondition constrains definition eligibility to one or more recurring local-time windows.

func (*ScheduleCondition) Evaluate added in v0.9.1

Evaluate returns the effective eligibility of the schedule at the provided time.

func (*ScheduleCondition) Validate added in v0.9.1

func (s *ScheduleCondition) Validate() error

Validate checks that the schedule condition is well-formed.

type ScheduleWindow added in v0.9.1

type ScheduleWindow struct {
	// Days limits the window to specific weekdays using stable short
	// names such as mon, tue, wed, thu, fri, sat, and sun. Empty means
	// every day.
	Days []string `yaml:"days,omitempty" json:"days,omitempty"`
	// Start is the local wall-clock start time in HH:MM 24-hour form.
	Start string `yaml:"start,omitempty" json:"start,omitempty"`
	// End is the local wall-clock end time in HH:MM 24-hour form. When
	// End is earlier than Start, the window crosses midnight into the
	// next day.
	End string `yaml:"end,omitempty" json:"end,omitempty"`
}

ScheduleWindow is one recurring local-time eligibility window.

func (ScheduleWindow) Validate added in v0.9.1

func (w ScheduleWindow) Validate() error

Validate checks that the schedule window is structurally valid.

type Spec added in v0.9.1

type Spec struct {
	// Name is the unique identifier for the loop. Required.
	Name string `yaml:"name,omitempty" json:"name,omitempty"`

	// Enabled marks the definition as eligible for runtime lifecycle
	// management. Service definitions only auto-start when enabled.
	Enabled bool `yaml:"enabled" json:"enabled"`

	// Task is the static prompt for each iteration. Ignored when
	// TaskBuilder is set.
	Task string `yaml:"task,omitempty" json:"task,omitempty"`

	// Profile shapes loop execution: routing hints, context-injection
	// tags, tool exclusions, and related request-shaping guidance.
	Profile router.LoopProfile `yaml:"profile,omitempty" json:"profile,omitempty"`

	// Operation describes the runtime pattern expected for the loop.
	Operation Operation `yaml:"operation,omitempty" json:"operation,omitempty"`

	// Completion describes how results should be delivered back to a
	// caller, conversation, or channel.
	Completion Completion `yaml:"completion,omitempty" json:"completion,omitempty"`

	// Conditions constrain when the definition is currently eligible to
	// run or launch. When empty, the definition is always eligible
	// unless blocked by policy.
	Conditions Conditions `yaml:"conditions,omitempty" json:"conditions,omitempty"`

	// Tags are capability tags for tool scoping. When non-empty,
	// the loop's tool registry is filtered to tools matching these
	// tags (plus always-active tags).
	Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`

	// ExcludeTools lists tool names to exclude from the loop's
	// available tools.
	ExcludeTools []string `yaml:"exclude_tools,omitempty" json:"exclude_tools,omitempty"`

	// SleepMin is the minimum sleep duration between iterations.
	SleepMin time.Duration `yaml:"sleep_min,omitempty" json:"sleep_min,omitempty"`
	// SleepMax is the maximum sleep duration between iterations.
	SleepMax time.Duration `yaml:"sleep_max,omitempty" json:"sleep_max,omitempty"`
	// SleepDefault is the initial sleep duration before the loop
	// self-adjusts.
	SleepDefault time.Duration `yaml:"sleep_default,omitempty" json:"sleep_default,omitempty"`
	// Jitter randomizes sleep durations to break periodicity.
	Jitter *float64 `yaml:"jitter,omitempty" json:"jitter,omitempty"`

	// MaxDuration is the maximum wall-clock time the loop may run.
	MaxDuration time.Duration `yaml:"max_duration,omitempty" json:"max_duration,omitempty"`
	// MaxIter is the maximum number of iteration attempts the loop
	// may make.
	MaxIter int `yaml:"max_iter,omitempty" json:"max_iter,omitempty"`

	// Supervisor enables frontier model dice rolls.
	Supervisor bool `yaml:"supervisor,omitempty" json:"supervisor,omitempty"`
	// SupervisorProb is the probability of using the supervisor model.
	SupervisorProb float64 `yaml:"supervisor_prob,omitempty" json:"supervisor_prob,omitempty"`
	// QualityFloor is the minimum model quality rating for normal
	// iterations.
	QualityFloor int `yaml:"quality_floor,omitempty" json:"quality_floor,omitempty"`
	// SupervisorContext is prepended during supervisor iterations.
	SupervisorContext string `yaml:"supervisor_context,omitempty" json:"supervisor_context,omitempty"`
	// SupervisorQualityFloor is the quality floor for supervisor
	// iterations.
	SupervisorQualityFloor int `yaml:"supervisor_quality_floor,omitempty" json:"supervisor_quality_floor,omitempty"`

	// OnRetrigger determines behavior when the loop is triggered again
	// while already running.
	OnRetrigger RetriggerMode `yaml:"on_retrigger,omitempty" json:"on_retrigger,omitempty"`

	// TaskBuilder generates a prompt per-iteration.
	TaskBuilder func(ctx context.Context, isSupervisor bool) (string, error) `yaml:"-" json:"-"`

	// PostIterate runs after each successful iteration.
	PostIterate func(ctx context.Context, result IterationResult) error `yaml:"-" json:"-"`

	// WaitFunc blocks until an external event arrives.
	WaitFunc func(ctx context.Context) (any, error) `yaml:"-" json:"-"`

	// Handler processes an iteration directly without an LLM call.
	Handler func(ctx context.Context, event any) error `yaml:"-" json:"-"`

	// Hints are merged into Request hints for each iteration.
	Hints map[string]string `yaml:"hints,omitempty" json:"hints,omitempty"`

	// FallbackContent is static text used when the loop completes a
	// request/reply run without any user-visible content. Interactive
	// loops can set this to guarantee a reply.
	FallbackContent string `yaml:"fallback_content,omitempty" json:"fallback_content,omitempty"`

	// Setup is called by the registry spawn helpers after [New] or
	// [NewFromSpec] but before [Loop.Start].
	Setup func(l *Loop) `yaml:"-" json:"-"`

	// Metadata holds arbitrary key/value pairs for the loop.
	Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`

	// ParentID is the parent loop ID, if any.
	ParentID string `yaml:"parent_id,omitempty" json:"parent_id,omitempty"`
}

Spec is the loops-ng contract for describing a loop. It carries both the current engine-facing config fields and the forward-looking loops-ng semantics. Today it compiles to Config, while [Profile] already shapes requests for loops created via NewFromSpec. Operation and Completion are retained for the upcoming RunV2 work.

func (*Spec) EffectiveConfig added in v0.9.1

func (s *Spec) EffectiveConfig() Config

EffectiveConfig returns the engine-facing configuration for this spec with loop runtime defaults applied. This is useful for inspection, linting, and warning surfaces that need to explain what a partially specified definition will actually do at runtime.

func (Spec) MarshalJSON added in v0.9.1

func (s Spec) MarshalJSON() ([]byte, error)

MarshalJSON renders a loops-ng spec in a human-facing contract shape suitable for APIs and tools: durations are strings and retrigger mode is named instead of using the engine's integer form.

func (*Spec) ToConfig added in v0.9.1

func (s *Spec) ToConfig() Config

ToConfig compiles the current engine-facing portion of a Spec into today's Config shape. [Spec.Operation] and [Spec.Completion] already flow through into the runtime config, while [Spec.Profile] remains a request-shaping layer applied by NewFromSpec rather than a field on Config.

func (*Spec) UnmarshalJSON added in v0.9.1

func (s *Spec) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the same human-facing contract shape emitted by Spec.MarshalJSON.

func (*Spec) Validate added in v0.9.1

func (s *Spec) Validate() error

Validate checks that the loops-ng-facing fields and the current engine-facing configuration are internally consistent.

func (*Spec) ValidatePersistable added in v0.9.1

func (s *Spec) ValidatePersistable() error

ValidatePersistable checks that the spec is valid and safe to store in config or a persistent overlay. Persisted loop definitions are data, not code, so runtime-only hooks must remain nil.

type State

type State string

State represents the lifecycle state of a running loop.

const (
	// StatePending means the loop is registered but not yet started
	// (e.g., waiting for a StartWhen condition).
	StatePending State = "pending"
	// StateSleeping means the loop is between iterations, waiting for
	// the next wake.
	StateSleeping State = "sleeping"
	// StateWaiting means the loop is blocked on a WaitFunc, waiting
	// for an external event to trigger the next iteration.
	StateWaiting State = "waiting"
	// StateProcessing means the loop is actively running an iteration
	// (LLM call or Handler execution).
	StateProcessing State = "processing"
	// StateError means the loop's last iteration failed. It will
	// retry on the next sleep cycle.
	StateError State = "error"
	// StateStopped means the loop has been cancelled and is no
	// longer running.
	StateStopped State = "stopped"
)

Loop states.

type Status

type Status struct {
	// ID is the unique loop identifier.
	ID string `json:"id"`
	// Name is the human-readable loop name.
	Name string `json:"name"`
	// State is the current lifecycle state.
	State State `json:"state"`
	// ParentID is the ID of the parent loop, if any.
	ParentID string `json:"parent_id,omitempty"`
	// StartedAt is when the loop was started.
	StartedAt time.Time `json:"started_at"`
	// LastWakeAt is when the loop last began an iteration.
	LastWakeAt time.Time `json:"last_wake_at,omitempty"`
	// Iterations is the total number of completed (successful) iterations.
	Iterations int `json:"iterations"`
	// Attempts is the total number of iteration attempts (including failures).
	Attempts int `json:"attempts"`
	// TotalInputTokens is the cumulative input tokens across all iterations.
	TotalInputTokens int `json:"total_input_tokens"`
	// TotalOutputTokens is the cumulative output tokens across all iterations.
	TotalOutputTokens int `json:"total_output_tokens"`
	// LastInputTokens is the input token count from the most recent iteration.
	LastInputTokens int `json:"last_input_tokens,omitempty"`
	// LastOutputTokens is the output token count from the most recent iteration.
	LastOutputTokens int `json:"last_output_tokens,omitempty"`
	// ContextWindow is the maximum context size (in tokens) of the model used.
	ContextWindow int `json:"context_window,omitempty"`
	// LastError is the error message from the most recent failed iteration.
	LastError string `json:"last_error,omitempty"`
	// ConsecutiveErrors is the number of consecutive failed iterations.
	ConsecutiveErrors int `json:"consecutive_errors"`
	// RecentConvIDs holds conversation IDs from the most recent iterations
	// (up to 10), newest first. Used by the visualizer to query log entries
	// scoped to this loop.
	RecentConvIDs []string `json:"recent_conv_ids,omitempty"`
	// HandlerOnly is true when the loop uses a Handler instead of LLM
	// iterations. Handler-only loops have no token metrics.
	HandlerOnly bool `json:"handler_only,omitempty"`
	// EventDriven is true when the loop uses a WaitFunc instead of
	// timer-based sleeping.
	EventDriven bool `json:"event_driven,omitempty"`
	// RecentIterations holds up to 10 completed iteration snapshots
	// (newest first), used by the dashboard timeline.
	RecentIterations []IterationSnapshot `json:"recent_iterations,omitempty"`
	// LastSupervisorIter is the iteration number of the most recent
	// successful supervisor iteration. Zero means no supervisor
	// iteration has completed yet.
	LastSupervisorIter int `json:"last_supervisor_iter,omitempty"`
	// LLMContext holds enrichment data from the most recent
	// loop_llm_start event (model, est_tokens, messages, tools,
	// complexity, intent, reasoning). Only populated while the loop
	// is in processing state, so late-connecting dashboard clients
	// can display it immediately.
	LLMContext map[string]any `json:"llm_context,omitempty"`
	// ActiveTags holds the currently active capability tags at the time
	// of the snapshot. Nil when capability tagging is not configured.
	ActiveTags []string `json:"active_tags,omitempty"`
	// Tooling captures the resolved loop-level tool/capability state.
	Tooling ToolingState `json:"tooling,omitempty"`
	// Config is a copy of the loop's configuration.
	Config Config `json:"config"`
}

Status is a snapshot of a loop's current state and metrics, suitable for external inspection via the registry.

type StreamCallback

type StreamCallback func(event any)

StreamCallback receives streaming events. Nil disables streaming.

type ToolingState added in v0.9.1

type ToolingState struct {
	ConfiguredTags     []string                            `json:"configured_tags,omitempty"`
	LoadedTags         []string                            `json:"loaded_tags,omitempty"`
	LoadedCapabilities []toolcatalog.LoadedCapabilityEntry `json:"loaded_capabilities,omitempty"`
	EffectiveTools     []string                            `json:"effective_tools,omitempty"`
	ExcludedTools      []string                            `json:"excluded_tools,omitempty"`
	ToolsUsed          map[string]int                      `json:"tools_used,omitempty"`
}

ToolingState captures the resolved tool/capability surface for a loop or a single historical iteration. It gives the dashboard one authoritative payload to render instead of re-deriving state from a mix of config, live context, and snapshot arrays.

func BuildToolingState added in v0.9.1

func BuildToolingState(configuredTags, loadedTags, effectiveTools, excludedTools []string, loadedCapabilities []toolcatalog.LoadedCapabilityEntry, toolsUsed map[string]int) ToolingState

BuildToolingState normalizes a loop or iteration tool/capability snapshot into a stable, sorted payload for the API and dashboard.

type UnknownDefinitionError added in v0.9.1

type UnknownDefinitionError struct {
	Name string
}

UnknownDefinitionError reports a missing dynamic loop definition.

func (*UnknownDefinitionError) Error added in v0.9.1

func (e *UnknownDefinitionError) Error() string

Jump to

Keyboard shortcuts

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