goal

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package goal is the agent's first desired-state kind: a Goal declares an objective and a stop condition (the desired state), and a reconciler drives it toward that condition by dispatching work steps and observing progress (the observed state). It is the agent's own execution model expressed on the generic resource + reconcile foundation: declarative, level-triggered, crash-resumable, and budget-bounded, rather than an imperative do-step-do-step loop that loses the thread on failure.

Index

Constants

View Source
const (
	// GroupVersion is the Goal kind's API group and version.
	GroupVersion = "goal.ionagent.io/v1alpha1"
	// Kind is the resource kind name.
	Kind = "Goal"
	// Finalizer is the key the reconciler adds to a goal so it gets a chance to
	// clean up owned work (child goals, runs, worktrees) before the goal is removed.
	Finalizer = "goal.ionagent.io/cleanup"
	// StepJobKind is the job kind a dispatched goal step is enqueued under.
	StepJobKind = "goal.step"
)
View Source
const (
	CondReady       = "Ready"       // True once the goal has converged
	CondReconciling = "Reconciling" // True while the controller is actively working
	CondStalled     = "Stalled"     // True when progress has stopped abnormally
)

Standard condition types (kstatus-style: abnormal conditions are present and True only when something noteworthy holds).

View Source
const (
	DefaultRetryBase    = 2 * time.Second
	DefaultRetryCeiling = 5 * time.Minute
)

DefaultRetryBase and DefaultRetryCeiling bound the exponential backoff a worker applies when a step fails: the first retry waits DefaultRetryBase, each later retry doubles, capped at DefaultRetryCeiling. Backing off matters because a step calls the model and external tools; retrying a persistently failing step with no delay would burn the attempt budget in microseconds and hammer those services.

View Source
const DefaultLease = 5 * time.Minute

DefaultLease is how long a claimed step is leased before, if the worker has not completed or renewed it, the queue treats the worker as crashed and re-leases the step to another worker. That re-lease is the crash-recovery path.

View Source
const DefaultMaxSteps = 20

DefaultMaxSteps bounds how many steps a goal may spend when its spec sets no MaxSteps, so a goal that never converges stalls instead of burning budget forever.

View Source
const DefaultPollInterval = 15 * time.Second

DefaultPollInterval is how often the reconciler re-checks an in-flight step when it is not woken by a completion signal (the safety-net poll).

View Source
const DefaultWaitRecheckFactor = 10

DefaultWaitRecheckFactor scales the poll interval into the recheck fallback for a parked goal: a goal waiting on a fan-out is normally woken the moment a child settles, so the timed re-check only covers a lost wake and can be much slower than the in-flight poll. That gap is what makes a wait cheap: re-check jobs are paced by child completions, not by the poll.

View Source
const StepQueue = "goal-steps"

StepQueue is the job queue goal steps are dispatched on, so a step worker claims only goal steps and not unrelated jobs.

View Source
const StepSubject = "goal.step.done"

StepSubject is the bus subject a worker publishes on when a step completes, so the reconciler is woken to re-evaluate promptly (the resync is the fallback).

Variables

View Source
var ErrWaiting = errors.New("goal: step waiting on external state")

ErrWaiting is returned by a StepExecutor whose step made no progress because it is waiting on external state, such as a fan-out whose children are still running. The worker completes the job without persisting a checkpoint and stamps Status.WaitingSince, and the reconciler then parks the goal: no step is counted against the budget and no re-check job is dispatched until a child settles (the wake) or the recheck fallback fires. Without this, a wait is a full durable step per poll cycle, and a long enough wait exhausts the step budget and false-stalls the goal.

Functions

func RegisterKind

func RegisterKind(reg *resource.Registry) error

RegisterKind registers the Goal kind so goals can be stored and admitted like any other resource.

Types

type Cleaner

type Cleaner interface {
	Cleanup(ctx context.Context, r resource.Resource) error
}

Cleaner runs the teardown a goal's finalizer guards (remove a worktree, cancel a run, delete child goals) before the goal is allowed to be deleted. A nil Cleaner means there is nothing external to clean up.

type Condition

type Condition = resource.Condition

Condition is one standard status condition (the shared resource.Condition).

type InFlight

type InFlight struct {
	JobID     string    `json:"jobID"`
	StartedAt time.Time `json:"startedAt"`
}

InFlight records a dispatched step not yet observed complete, so a re-reconcile observes the running work instead of launching a duplicate.

type Option

type Option func(*Reconciler)

Option configures a Reconciler.

func WithCleaner

func WithCleaner(c Cleaner) Option

WithCleaner sets the teardown hook run before a goal's finalizer is removed.

func WithPollInterval

func WithPollInterval(d time.Duration) Option

WithPollInterval overrides the in-flight re-check interval.

func WithStepMaxAttempts

func WithStepMaxAttempts(n int) Option

WithStepMaxAttempts bounds how many times a single dispatched step is retried by the job queue before it goes dead and stalls the goal (0 uses the queue default).

func WithWaitRecheck

func WithWaitRecheck(d time.Duration) Option

WithWaitRecheck overrides how long a parked goal (waiting on a fan-out's children) may sit before the reconciler re-checks it without a wake signal.

func WithWakeBus

func WithWakeBus(b bus.Bus) Option

WithWakeBus sets the bus the reconciler signals a parked owner on when one of its children settles, so a fan-out parent re-checks on child state-change instead of waiting out the recheck fallback.

type Phase

type Phase string

Phase is a coarse, human-facing lifecycle summary of a goal, derived from its conditions (a convenience projection, not the source of truth).

const (
	PhasePending   Phase = "Pending"   // accepted, no step run yet
	PhaseRunning   Phase = "Running"   // a step is in flight or more are queued
	PhaseConverged Phase = "Converged" // the stop condition is satisfied
	PhaseStalled   Phase = "Stalled"   // out of budget or a step failed terminally
)

The phases a goal moves through, from accepted to a terminal converged or stalled state.

type Reconciler

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

Reconciler drives a Goal toward its stop condition. It never runs the work itself: it dispatches a step to the durable job queue, records it in flight so a re-reconcile observes rather than relaunches, and re-evaluates when the step completes. That keeps each reconcile quick and idempotent, and because progress is recorded in status, a crash resumes mid-goal instead of restarting.

func NewReconciler

func NewReconciler(store resource.Store, q jobs.Queue, clk clock.Clock, stop StopEvaluator, opts ...Option) *Reconciler

NewReconciler builds a Reconciler over the given store, job queue, clock and stop evaluator.

func (*Reconciler) Reconcile

func (g *Reconciler) Reconcile(ctx context.Context, ref reconcile.Ref) (reconcile.Result, error)

Reconcile drives one goal one level-triggered step toward its desired state.

type Spec

type Spec struct {
	Objective     string `json:"objective"`
	StopCondition string `json:"stopCondition"`
	MaxSteps      int    `json:"maxSteps,omitempty"`
	// Grant is the exact set of action names this goal may take, carried on the goal
	// itself so authority travels with the work rather than being fixed at the
	// executor. One executor can then drive goals of differing authority (a parent at
	// full grant, a delegated child narrowed to a subset), and a child's grant is set
	// to a subset of its parent's so a delegation can never widen authority. Empty
	// defers to the executor's default grant, so an ungoverned standalone run is
	// unchanged.
	Grant []string `json:"grant,omitempty"`
	// Depth is how many delegation hops separate this goal from a root goal: a root
	// is 0 and a spawned child is its parent's depth plus one. A fan-out spawner
	// refuses to create a child past a maximum depth, so a chain of agents spawning
	// agents cannot recurse without bound.
	Depth int `json:"depth,omitempty"`
	// BudgetPool is the run id whose budget this goal charges and reserves against.
	// A fan-out shares one pool: every descendant inherits the root's pool, so the
	// whole graph is bounded by a single ceiling rather than a budget per goal. Empty
	// means the goal is its own pool (a standalone root).
	BudgetPool string `json:"budgetPool,omitempty"`
	// System is the standing system prompt this goal runs under, carried on the goal
	// so a delegated child can run as a different agent than its parent (its prompt
	// baked in by the spawner from the bound Agent). Empty defers to the executor's
	// default prompt, so a standalone run is unchanged.
	System string `json:"system,omitempty"`
	// Driver names the run loop this goal uses (resolved from the driver registry), and
	// Model names the model it runs on. They are carried on the goal so a delegated
	// child can run a different loop and model than its parent (set by the spawner from
	// the bound Agent). Empty defers to the host default loop and model, so a standalone
	// run is unchanged.
	Driver string `json:"driver,omitempty"`
	Model  string `json:"model,omitempty"`
	// Attachments are images seeded onto the goal's opening user turn. The
	// objective is the opening turn's text; the attachments are its images, so
	// a goal can open on a picture the way a composer prompt can. They are the
	// model port's own image type (bytes inline, like the rest of a
	// conversation in the checkpoint), so no translation is needed to open the
	// turn. Empty on every text-only goal, so the text path serializes
	// identically and is unchanged.
	Attachments []llm.Image `json:"attachments,omitempty"`
}

Spec is a goal's desired state: what to achieve, the condition that means it is done, an optional ceiling on how many steps may be spent trying, and the capabilities the goal is authorized to use.

func DecodeSpec

func DecodeSpec(r resource.Resource) (Spec, error)

DecodeSpec reads the typed spec from a resource.

type Status

type Status struct {
	Phase Phase `json:"phase,omitempty"`
	// ObservedSpecHash is the resource.SpecHash the reconciler last acted on, so a
	// reconcile is a no-op while the spec is unchanged and the goal has settled.
	ObservedSpecHash string      `json:"observedSpecHash,omitempty"`
	Steps            int         `json:"steps,omitempty"`
	InFlight         *InFlight   `json:"inFlight,omitempty"`
	Conditions       []Condition `json:"conditions,omitempty"`
	Message          string      `json:"message,omitempty"`
	// Checkpoint is opaque progress a worker persists mid-step so a step that
	// crashes resumes from here instead of restarting. It is owned by the step
	// executor; the reconciler never interprets it.
	Checkpoint json.RawMessage `json:"checkpoint,omitempty"`
	// WaitingSince marks the goal as parked: its last step reported ErrWaiting (no
	// progress, waiting on external state such as a fan-out's running children),
	// stamped with when the worker recorded it. While set, the reconciler does not
	// dispatch a step, evaluate the stop condition, or count the wait against the
	// step budget. A settling child clears it (the prompt wake); the reconciler's
	// recheck fallback clears it after a bounded delay if that wake is lost.
	WaitingSince *time.Time `json:"waitingSince,omitempty"`
}

Status is a goal's observed state.

func DecodeStatus

func DecodeStatus(r resource.Resource) (Status, error)

DecodeStatus reads the typed status from a resource.

func (Status) Encode

func (s Status) Encode() (json.RawMessage, error)

Encode marshals the status for writing back onto a resource.

func (*Status) SetCondition

func (s *Status) SetCondition(c Condition, now time.Time)

SetCondition upserts c by type, stamping LastTransitionTime only when the status value actually changes (so a no-op reconcile does not churn the time).

type StepExecutor

type StepExecutor interface {
	Execute(ctx context.Context, goal resource.Resource) (checkpoint json.RawMessage, err error)
}

StepExecutor performs one step of work toward a goal: it is where the model is called, tools are run, and sub-goals are planned. It is handed the goal resource (whose status carries the last Checkpoint) and returns a new checkpoint to persist. It MUST be safe to re-run after a crash: a re-leased step calls Execute again with the persisted checkpoint, so the executor resumes from it rather than repeating finished work. The real executor is wired in with the conversation loop; the foundation here is provider-agnostic.

type StopEvaluator

type StopEvaluator interface {
	Met(ctx context.Context, spec Spec, status Status) (met bool, reason string, err error)
}

StopEvaluator decides whether a goal's stop condition is satisfied given its desired spec and observed status. The production evaluator asks the model; tests supply a deterministic one. It is the agent's semantic convergence test, the thing a numeric controller has no equivalent for.

type Worker

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

Worker claims dispatched goal steps and runs them through a StepExecutor. It is the execution half of the goal reconciler's dispatch-and-observe loop: the reconciler decides a step is needed and enqueues it; the worker performs it, persists progress, and signals completion so the reconciler observes the result.

func NewWorker

func NewWorker(store resource.Store, q jobs.Queue, clk clock.Timing, exec StepExecutor, opts ...WorkerOption) *Worker

NewWorker builds a goal-step worker over the store, queue and executor. The clock is used to schedule retry backoff and must be the same clock the queue uses, so a failed step's RunAt is comparable to the queue's claim time.

func (*Worker) ProcessOnce

func (w *Worker) ProcessOnce(ctx context.Context) (bool, error)

ProcessOnce claims at most one ready step and runs it, reporting whether a step was processed. It is the unit of work a Run loop repeats and the entry point tests drive deterministically.

func (*Worker) Run

func (w *Worker) Run(ctx context.Context, poll time.Duration)

Run processes steps until ctx is cancelled. When the queue implements jobs.Waker, an idle worker wakes on the enqueue signal, so dispatch-to-claim latency is signal delivery, not the poll interval. The poll remains the always-correct floor: it is the only wake-up for scheduled RunAt arrivals, expired leases, and enqueues from processes the queue cannot observe.

type WorkerOption

type WorkerOption func(*Worker)

WorkerOption configures a Worker.

func WithBackoff

func WithBackoff(base, ceiling time.Duration) WorkerOption

WithBackoff overrides the failed-step retry backoff (base delay and ceiling).

func WithBus

func WithBus(b bus.Bus) WorkerOption

WithBus sets the bus a worker publishes step-completion signals on.

func WithLease

func WithLease(d time.Duration) WorkerOption

WithLease overrides the step lease duration.

Jump to

Keyboard shortcuts

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