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 ¶
- Variables
- type Budgets
- type CampaignSnapshot
- type CampaignState
- func (c *CampaignState) Abort() error
- func (c *CampaignState) Activate(id string) error
- func (c *CampaignState) Block(id string) error
- func (c *CampaignState) Cancel() error
- func (c *CampaignState) Complete(id string) error
- func (c *CampaignState) CompleteByEvidence(id string) error
- func (c *CampaignState) Cost() float64
- func (c *CampaignState) Eligible(id string) (bool, error)
- func (c *CampaignState) Fail(id string) error
- func (c *CampaignState) FailureCount(id string) int
- func (c *CampaignState) RecordCost(amount float64) error
- func (c *CampaignState) RecordFailure(id string) error
- func (c *CampaignState) RecordStep() error
- func (c *CampaignState) Skip(id string) error
- func (c *CampaignState) Snapshot() CampaignSnapshot
- func (c *CampaignState) Steps() int
- func (c *CampaignState) StopReason() (StopReason, bool)
- func (c *CampaignState) Stopped() bool
- func (c *CampaignState) TaskStatus(id string) (TaskStatus, error)
- type ContentPredicate
- type ContentRules
- type Criteria
- type Goal
- type StopReason
- type Task
- type TaskStatus
Constants ¶
This section is empty.
Variables ¶
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.
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
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
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
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 ¶
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.