goal

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package goal is Chatwright's goal/task/budget contract for goal-driven AI testing: the campaign's product-level intent (Goal), its trackable units of work (Task) with dependencies and prose success criteria, the limits that bound an autonomous run (Budgets), and the guarded state machine that tracks progress against them (CampaignState).

This package is a pure contract: no AI, no emulator, no I/O. It has no opinion on how a task is attempted — only on what a valid Goal looks like and which state transitions a campaign may legally make. The observe-plan-act-validate loop that drives an AI actor through a CampaignState (package actor) is a later slice. This package does depend on observe (for Criteria's Observation parameter — see Task.Criteria): observe is itself I/O-free, a pure projection over data the platform package already read, so this dependency does not compromise "no I/O"; it stays one-directional (observe never imports goal).

Typical use:

g := goal.Goal{
	ID:    "listus-shopping-list",
	Title: "Exercise the shopping-list lifecycle",
	Tasks: []goal.Task{
		{ID: "onboarding", SuccessCriteria: "user completes language selection"},
		{ID: "add-items", DependsOn: []string{"onboarding"}, SuccessCriteria: "several items visible in the list"},
	},
	Budgets: goal.Budgets{MaxSteps: 80, MaxDuration: 10 * time.Minute, MaxRepeatedFailures: 3},
}
campaign, err := goal.NewCampaignState(g, time.Now)
// campaign.Activate("onboarding") ... campaign.Complete("onboarding") ...

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrEmptyTaskID means a Task's ID is empty or whitespace-only.
	ErrEmptyTaskID = errors.New("goal: task id is empty")
	// ErrDuplicateTaskID means two Tasks in the same Goal share an ID.
	ErrDuplicateTaskID = errors.New("goal: duplicate task id")
	// ErrUnknownDependency means a Task.DependsOn entry does not name a
	// Task ID present in the same Goal.
	ErrUnknownDependency = errors.New("goal: unknown dependency")
	// ErrDependencyCycle means the Task dependency graph contains a cycle.
	ErrDependencyCycle = errors.New("goal: dependency cycle")
	// ErrNegativeBudget means a Budgets field that must be zero (unlimited)
	// or positive was set negative.
	ErrNegativeBudget = errors.New("goal: budget must not be negative")
	// ErrNonPositiveCostBudget means Budgets.MaxCost was set to zero or a
	// negative value; leave it nil to mean "not budgeted".
	ErrNonPositiveCostBudget = errors.New("goal: max cost budget must be positive when set")
)

Goal.Validate errors.

View Source
var (
	// ErrNilClock means NewCampaignState was called without a clock
	// function.
	ErrNilClock = errors.New("goal: clock function is nil")
	// ErrUnknownTask means a task id does not belong to the campaign's Goal.
	ErrUnknownTask = errors.New("goal: unknown task id")
	// ErrTaskNotEligible means Activate was called on a Pending task whose
	// DependsOn tasks are not all Completed.
	ErrTaskNotEligible = errors.New("goal: task is not eligible (unmet dependencies)")
	// ErrTaskNotActivatable means Activate was called on a task that was
	// not Pending — including an already-Active or already-terminal task.
	ErrTaskNotActivatable = errors.New("goal: task is not activatable")
	// ErrTaskNotActive means Complete, Fail, Block or Skip was called on a
	// task that was not currently Active.
	ErrTaskNotActive = errors.New("goal: task is not active")
	// ErrCampaignStopped means a mutating method was called after the
	// campaign had already stopped.
	ErrCampaignStopped = errors.New("goal: campaign has already stopped")
	// ErrNegativeCost means RecordCost was called with a negative amount.
	ErrNegativeCost = errors.New("goal: cost amount must not be negative")
)

CampaignState errors.

Functions

This section is empty.

Types

type Budgets

type Budgets struct {
	// MaxSteps caps the number of steps CampaignState.RecordStep counts.
	// Zero means unlimited.
	MaxSteps int `json:"maxSteps"`

	// MaxDuration caps wall-clock time elapsed since the campaign started,
	// measured by the CampaignState's injected clock. Zero means unlimited.
	MaxDuration time.Duration `json:"maxDurationNanoseconds"`

	// MaxRepeatedFailures caps how many times CampaignState.RecordFailure
	// may be called for a single task before the campaign stops. Zero means
	// unlimited.
	MaxRepeatedFailures int `json:"maxRepeatedFailures"`

	// MaxCost optionally caps spend against the campaign (tokens, currency
	// or another caller-defined unit — whatever unit the caller accrues via
	// CampaignState.RecordCost). Nil means cost is not budgeted.
	MaxCost *float64 `json:"maxCost"`
}

Budgets bounds one campaign run. Every numeric field's zero value means "no limit"; a negative value is invalid. MaxCost is the one genuinely optional field: nil means cost is not budgeted at all.

type CampaignSnapshot

type CampaignSnapshot struct {
	GoalID     string
	Statuses   map[string]TaskStatus
	Steps      int
	Cost       float64
	Elapsed    time.Duration
	Failures   map[string]int
	Stopped    bool
	StopReason StopReason
}

CampaignSnapshot is a detached, point-in-time copy of a CampaignState's progress: safe to retain, log or compare after the originating CampaignState has moved on.

type CampaignState

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

CampaignState is the guarded runtime state machine for one Goal: task statuses, elapsed steps and duration, per-task failure counts, and the deterministic StopReason that ends the campaign. It performs no AI, networking or platform I/O — callers report progress in with RecordStep, RecordFailure and the task transition methods, and read state back out.

All methods are safe for concurrent use. Time comes from an injected clock (see NewCampaignState) rather than time.Now, so tests are deterministic and reproducible.

func NewCampaignState

func NewCampaignState(g Goal, now func() time.Time) (*CampaignState, error)

NewCampaignState validates g (see Goal.Validate) and starts a new campaign with every task Pending. now supplies the current time for step duration and budget checks; pass a fixed or fake clock in tests so duration-budget behaviour is deterministic. now must not be nil.

func (*CampaignState) Abort

func (c *CampaignState) Abort() error

Abort stops the campaign with StopError, after an unrecoverable runtime failure the caller cannot attribute to a budget or an explicit cancellation. It errors if the campaign has already stopped.

func (*CampaignState) Activate

func (c *CampaignState) Activate(id string) error

Activate transitions a Pending, dependency-satisfied task to Active. It errors if:

  • the campaign has already stopped (ErrCampaignStopped);
  • the task id is unknown (ErrUnknownTask);
  • the task is Pending but its dependencies are not all Completed (ErrTaskNotEligible);
  • the task is not Pending at all — including an already-Active or a terminal task (ErrTaskNotActivatable).

func (*CampaignState) Block

func (c *CampaignState) Block(id string) error

Block transitions an Active task to Blocked.

func (*CampaignState) Cancel

func (c *CampaignState) Cancel() error

Cancel stops the campaign with StopCancelled — an external decision to end the run early, distinct from any budget being exhausted. It errors if the campaign has already stopped.

func (*CampaignState) Complete

func (c *CampaignState) Complete(id string) error

Complete transitions an Active task to Completed — the actor's own task-done claim, or any other caller-driven completion. If this transition is what leaves every task terminal, the campaign stops with StopGoalComplete. See CompleteByEvidence for the loop's own machine-checkable-criteria completion path.

func (*CampaignState) CompleteByEvidence added in v0.3.0

func (c *CampaignState) CompleteByEvidence(id string) error

CompleteByEvidence transitions an Active task to Completed because the loop's own machine-checkable criteria evaluation found the task's success condition already holds (evidence-defined completion) — never because the actor itself proposed task-done (use Complete for that). It is otherwise identical to Complete: same guards, same TaskCompleted target. The only difference is which StopReason a resulting checkGoalComplete uses — StopGoalMetByEvidence instead of StopGoalComplete — so a report can tell "evidence closed the campaign out" from "the actor's own wrap-up did", per spec/ideas/evidence-defined-completion.md in the chatwright/chatwright standard repository.

func (*CampaignState) Cost

func (c *CampaignState) Cost() float64

Cost returns the total cost RecordCost has accrued so far.

func (*CampaignState) Eligible

func (c *CampaignState) Eligible(id string) (bool, error)

Eligible reports whether the task with the given id is currently Pending and every task it DependsOn is Completed — the guard Activate enforces. It errors if the task id is unknown.

func (*CampaignState) Fail

func (c *CampaignState) Fail(id string) error

Fail transitions an Active task to Failed.

func (*CampaignState) FailureCount

func (c *CampaignState) FailureCount(id string) int

FailureCount returns how many failures RecordFailure has counted against the given task id so far (zero for an unknown id, rather than an error — callers that need to distinguish "no failures" from "unknown task" should check TaskStatus first).

func (*CampaignState) RecordCost

func (c *CampaignState) RecordCost(amount float64) error

RecordCost accrues amount against the campaign's cost budget (tokens, currency or whatever unit Budgets.MaxCost was expressed in). Costs accumulate across calls for the life of the campaign, not per task — call it once per spend you want counted (e.g. once per actor Provider.Propose call, with that call's Usage.Cost). It stops the campaign deterministically with StopBudgetCost the moment accrued cost reaches a set, positive Budgets.MaxCost, and errors if the campaign has already stopped or amount is negative.

func (*CampaignState) RecordFailure

func (c *CampaignState) RecordFailure(id string) error

RecordFailure attributes one failed attempt to the task with the given id. Repeated failures against the same task accumulate across calls — the task does not need to be re-activated between them — and once the count reaches a positive Budgets.MaxRepeatedFailures the campaign stops with StopRepeatedFailure. It errors if the campaign has already stopped or the task id is unknown.

func (*CampaignState) RecordStep

func (c *CampaignState) RecordStep() error

RecordStep counts one action/step against the campaign's step and duration budgets. Call it once per recorded actor action or scenario step — never derive a step count from time.Now internally; this method's only notion of "now" is the injected clock. It stops the campaign deterministically (StopBudgetSteps, then StopBudgetDuration) the moment a positive budget is reached, and errors if the campaign has already stopped.

func (*CampaignState) Skip

func (c *CampaignState) Skip(id string) error

Skip transitions an Active task to Skipped.

func (*CampaignState) Snapshot

func (c *CampaignState) Snapshot() CampaignSnapshot

Snapshot returns a detached copy of the campaign's current progress.

func (*CampaignState) Steps

func (c *CampaignState) Steps() int

Steps returns the number of steps RecordStep has counted so far.

func (*CampaignState) StopReason

func (c *CampaignState) StopReason() (StopReason, bool)

StopReason returns the reason the campaign stopped and true, or ("", false) while the campaign is still running.

func (*CampaignState) Stopped

func (c *CampaignState) Stopped() bool

Stopped reports whether the campaign has stopped accepting mutations.

func (*CampaignState) TaskStatus

func (c *CampaignState) TaskStatus(id string) (TaskStatus, error)

TaskStatus returns the current status of the task with the given id, or an error wrapping ErrUnknownTask if no such task exists.

type ContentPredicate added in v0.3.0

type ContentPredicate func(ctx context.Context, text string) (ok bool, reason string, err error)

ContentPredicate is a custom, deterministic content check beyond ContentRules' own Vocabulary/DenyPatterns — the "predicate seam for custom rules" spec/ideas/proposal-content-constraints.md calls for. ok reports whether text is allowed; reason is a human-readable explanation used only when ok is false. A non-nil error means the check itself failed (not that text violated it) and is surfaced to the loop's caller, mirroring Criteria's own error convention.

type ContentRules added in v0.3.0

type ContentRules struct {
	// Vocabulary is a case-insensitive allowlist of terms: text.Check fails,
	// citing this reason, when text (lower-cased) contains none of them as
	// a substring. Empty means no vocabulary check. Exact substring
	// matching only, deliberately: the idea's "Not Doing" rules out
	// semantic matching in this deterministic layer — a test author who
	// wants word-boundary precision supplies a DenyPatterns/Predicate
	// entry instead.
	Vocabulary []string

	// DenyPatterns are regular expressions checked against the proposal's
	// raw (not lower-cased) text; any match blocks it, regardless of
	// Vocabulary. Checked before Vocabulary, so a denied pattern is always
	// the reported reason even when the text also happens to contain an
	// allowed term.
	DenyPatterns []*regexp.Regexp

	// Predicate is an optional custom deterministic check, run last (after
	// DenyPatterns and Vocabulary both pass). Nil means no predicate
	// check.
	Predicate ContentPredicate
}

ContentRules declares machine-checkable content rules for a Task's (or a Goal's) ProposeSendText proposals — the "missing symmetric half of evidence-defined completion" spec/ideas/proposal-content-constraints.md describes: Criteria judges the world after an action; ContentRules judges what the actor may say on the way in. All three dimensions are deterministic — no semantic/NLP judgement, per the idea's explicit "Not Doing".

The zero value means "no rule": every text proposal passes. See EffectiveContentRules for how a Task's own ContentRules (when non-empty) overrides its Goal's, per the idea's "task overriding goal" resolution of its own open question — documented there, not repeated per call site.

func EffectiveContentRules added in v0.3.0

func EffectiveContentRules(g Goal, t Task) ContentRules

EffectiveContentRules resolves which ContentRules apply to t within g: t.ContentRules when it is non-Empty(), otherwise g.ContentRules — "task overriding goal" (spec/ideas/proposal-content-constraints.md's own open question, resolved this way and documented here as its single source of truth). A Task never merges its rules with its Goal's; declaring any task-level rule takes the goal-level rule out of the picture entirely for that task.

func (ContentRules) Check added in v0.3.0

func (r ContentRules) Check(ctx context.Context, text string) (ok bool, reason string, err error)

Check judges text against r's rules, in the fixed order documented on each field (DenyPatterns, then Vocabulary, then Predicate — the first violation found is the one reported). ok is true, with an empty reason, when r is Empty() or text violates none of its rules. A non-nil error means Predicate itself failed, never that text violated a rule.

func (ContentRules) Empty added in v0.3.0

func (r ContentRules) Empty() bool

Empty reports whether r declares no rule at all — the condition EffectiveContentRules uses to decide whether a Task's own ContentRules overrides its Goal's.

type Criteria added in v0.3.0

type Criteria func(ctx context.Context, obs observe.Observation) (bool, error)

Criteria is an optional, machine-checkable predicate for a Task's completion: given the current observation, it reports whether the task's success condition already holds. This is the loop-side backstop spec/ideas/evidence-defined-completion.md describes, alongside (never instead of) the prose SuccessCriteria the actor itself reads — see Task.Criteria.

A Criteria closure may also express a datastate assertion via the existing datastate.Executor seam (spec/ideas/evidence-defined-completion.md's "and/or a datastate assertion") by capturing a *datastate.Runner and ignoring obs entirely; this package depends on neither datastate nor any concrete executor to keep that seam open without importing it here.

Returning (false, nil) means "not yet met" — the ordinary, expected outcome on most iterations, never treated as an error. A non-nil error means evaluation itself failed (e.g. a query execution error, as distinct from an assertion that ran and did not hold) and is surfaced to the caller driving the loop, not silently treated as "not met".

type Goal

type Goal struct {
	ID          string   `json:"id"`
	Title       string   `json:"title"`
	Description string   `json:"description"`
	Tasks       []Task   `json:"tasks"`
	Constraints []string `json:"constraints"`
	Budgets     Budgets  `json:"budgets"`

	// ContentRules is this goal's optional machine-checkable content seam
	// — Go-only, never serialised — applied to every Task that does not
	// declare its own (non-empty) ContentRules. See
	// EffectiveContentRules.
	ContentRules ContentRules `json:"-"`
}

Goal is one campaign's product-level intent: a natural-language outcome broken into Tasks, plus the Constraints and Budgets that bound how an actor may pursue it. A Goal describes intent, never platform mechanics — see the goal-and-task-contract feature's goal-does-not-leak-platform-mechanics acceptance criterion.

func (Goal) Validate

func (g Goal) Validate() error

Validate checks that g is well-formed:

  • every Task has a non-empty, unique ID;
  • every Task.DependsOn entry resolves to another Task ID in g;
  • the dependency graph is acyclic;
  • Budgets are non-negative, and MaxCost, if set, is positive.

NewCampaignState calls Validate during construction, so a CampaignState can never exist over an invalid Goal. Callers may also call it directly — for example to validate an authored goal before scheduling a campaign.

type StopReason

type StopReason string

StopReason is why a CampaignState stopped accepting further mutations. Every stop names exactly one reason, chosen deterministically by the condition that caused it.

const (
	// StopGoalComplete means every task reached a terminal status — there
	// is no more eligible work left to activate. It does not by itself mean
	// every task succeeded; read individual TaskStatus values for that.
	StopGoalComplete StopReason = "goal-complete"
	// StopBudgetSteps means Budgets.MaxSteps was reached.
	StopBudgetSteps StopReason = "budget-steps"
	// StopBudgetDuration means Budgets.MaxDuration elapsed.
	StopBudgetDuration StopReason = "budget-duration"
	// StopRepeatedFailure means Budgets.MaxRepeatedFailures was reached for
	// one task.
	StopRepeatedFailure StopReason = "repeated-failure"
	// StopBudgetCost means Budgets.MaxCost was reached via RecordCost.
	StopBudgetCost StopReason = "budget-cost"
	// StopCancelled means CampaignState.Cancel was called.
	StopCancelled StopReason = "cancelled"
	// StopError means CampaignState.Abort was called after an unrecoverable
	// runtime failure.
	StopError StopReason = "error"
	// StopGoalMetByEvidence means every task reached a terminal status, and
	// the transition that made the LAST one terminal was a
	// CampaignState.CompleteByEvidence call — the loop's own
	// machine-checkable criteria evaluation, not the actor's own task-done
	// claim, is what actually closed the campaign out. It is otherwise
	// exactly like StopGoalComplete (checkGoalComplete's own "every task is
	// terminal" condition): the distinct reason exists so a report can
	// name evidence, not the actor's own wrap-up, as what ended the run —
	// see CampaignState.CompleteByEvidence and
	// spec/ideas/evidence-defined-completion.md in the chatwright/chatwright
	// standard repository.
	StopGoalMetByEvidence StopReason = "goal-met-by-evidence"
)

Stop reasons. See CampaignState.StopReason.

type Task

type Task struct {
	ID              string   `json:"id"`
	Title           string   `json:"title"`
	DependsOn       []string `json:"dependsOn"`
	SuccessCriteria string   `json:"successCriteria"`
	Milestones      []string `json:"milestones"`

	// Criteria is this task's optional machine-checkable completion seam —
	// Go-only, never serialised (the wire's Task shape is unchanged: this
	// field carries no `json` tag other than "-"). When set, the loop
	// evaluates it after every executed action and completes the task
	// deterministically the moment it holds — see
	// spec/ideas/evidence-defined-completion.md. Nil means "prose only",
	// the pre-existing behaviour: the actor's own task-done proposal (and
	// budgets, as the ultimate backstop) are all that end the task.
	Criteria Criteria `json:"-"`

	// ContentRules is this task's optional machine-checkable content seam
	// — Go-only, never serialised. When set (non-empty), it overrides the
	// owning Goal's own ContentRules for this task entirely rather than
	// merging with it — see EffectiveContentRules and
	// spec/ideas/proposal-content-constraints.md's own open question on
	// rule scope ("task overriding goal").
	ContentRules ContentRules `json:"-"`
}

Task is one trackable unit of work inside a Goal. Success is judged by prose SuccessCriteria — the contract never prescribes the bot commands or callback data used to satisfy it. DependsOn names other Task IDs in the same Goal that must be Completed before this task becomes eligible for CampaignState.Activate. Milestones names checkpoints this task's completion may reach; the reporting layer, not this package, interprets them.

type TaskStatus

type TaskStatus string

TaskStatus is a task's position in its guarded lifecycle:

pending -> active -> completed | failed | blocked | skipped

Only CampaignState mutates a task's status, and only along that guard: a task must be activated before it can reach any terminal status, and every terminal status is final.

const (
	TaskPending   TaskStatus = "pending"
	TaskActive    TaskStatus = "active"
	TaskCompleted TaskStatus = "completed"
	TaskFailed    TaskStatus = "failed"
	TaskBlocked   TaskStatus = "blocked"
	TaskSkipped   TaskStatus = "skipped"
)

Task lifecycle statuses. See TaskStatus.

func (TaskStatus) Terminal

func (s TaskStatus) Terminal() bool

Terminal reports whether s is one of the lifecycle's terminal outcomes. Once a task reaches a terminal status no further transition is possible.

Jump to

Keyboard shortcuts

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