actor

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: 14 Imported by: 0

Documentation

Overview

Package actor is Chatwright's AI actor loop: the observe-plan-act-validate cycle that drives a goal.CampaignState through a conversation using a pluggable Provider.

The seam to any concrete model or vendor is exactly one interface — Provider — kept deliberately dumb: it reads a Prompt (goal/task context, the current observe.Observation, bounded recent history) and returns a Proposal. Every safety property lives in the loop, never in a Provider: budgets and stop reasons come from goal.CampaignState, click proposals are checked with observe.Engine.Validate, and Chatwright remains authoritative — an invalid proposal is recorded and re-prompted, never silently acted on. See Loop.

Index

Constants

This section is empty.

Variables

View Source
var ErrCassetteCacheMiss = errors.New("actor: replay cache miss")

ErrCassetteCacheMiss means a ModeReplay CassetteProvider was asked to propose for a prompt its cassette has no recorded entry for.

View Source
var ErrNilClock = errors.New("actor: clock function is nil")

ErrNilClock means Config.Now was nil.

View Source
var ErrScriptExhausted = errors.New("actor: scripted provider's script is exhausted")

ErrScriptExhausted means a ScriptedProvider's Propose was called more times than its script has entries.

Functions

This section is empty.

Types

type ActionOutcome

type ActionOutcome struct {
	Kind ActionOutcomeKind `json:"kind"`
	// Detail is a human-readable explanation, set for
	// ActionSkippedInvalid/ActionResolutionFailed (why), empty otherwise.
	Detail string `json:"detail"`
}

ActionOutcome is what actually happened when the loop tried to act on a Proposal.

type ActionOutcomeKind

type ActionOutcomeKind string

ActionOutcomeKind classifies what happened when the loop acted on a proposal, or why it did not act at all. It is a string type, not an int enum, so it marshals to human-readable JSON (see AGENTS.md's "JSON artefacts carry human-readable string constants" convention) rather than a bare, meaningless integer.

const (
	// ActionSkippedInvalid: the proposal failed validation (a stale click)
	// or was malformed; the loop never submitted anything to the platform.
	ActionSkippedInvalid ActionOutcomeKind = "skipped-invalid"
	// ActionExecuted: the proposed action was submitted to the platform and
	// produced a semantically observable change: a new message, or an
	// existing message whose text or action labels actually differ from
	// before (see observedEffect/semanticallyEqualMessage in loop.go).
	ActionExecuted ActionOutcomeKind = "executed"
	// ActionExecutedNoEffect: the proposed action was submitted, but the
	// next observation showed no semantic change — either genuinely no
	// observe.Change at all, or the only bot-authored Changes were
	// content-identical re-renders (e.g. a message re-edited in place with
	// byte-identical text and the same actions, which still bumps Version
	// and so still appears as an observe.Change — see
	// observedEffect/semanticallyEqualMessage in loop.go). This is
	// deliberately the same outcome kind either way: from a Provider's or a
	// report's point of view, "the platform re-showed exactly what was
	// already there" is not progress, regardless of whether observe's own
	// Version bookkeeping ticked over. It is what feeds NonProgressLimit.
	ActionExecutedNoEffect ActionOutcomeKind = "executed-no-effect"
	// ActionResolutionFailed: a freshly validated proposal that the loop
	// could not resolve to a concrete platform action — e.g. no button on
	// the current message carries the validated action's label (see Loop's
	// single-live-surface scoping note). This counts as a task failure
	// (goal.CampaignState.RecordFailure).
	ActionResolutionFailed ActionOutcomeKind = "resolution-failed"
	// ActionTaskCompleted: a ProposeTaskDone proposal was accepted;
	// goal.CampaignState.Complete was called for the task.
	ActionTaskCompleted ActionOutcomeKind = "task-completed"
	// ActionTaskGivenUp: a ProposeGiveUp proposal was accepted;
	// goal.CampaignState.Fail was called for the task.
	ActionTaskGivenUp ActionOutcomeKind = "task-given-up"
	// ActionBlockedConstraintViolation: a ProposeSendText proposal's text
	// violated the active task's (or goal's) machine-checkable content
	// rules (goal.EffectiveContentRules) — a vocabulary allowlist, a
	// deny-pattern or a custom predicate. The loop never submitted it to
	// the platform; see campaign.FindingConstraintViolation and
	// spec/ideas/proposal-content-constraints.md.
	ActionBlockedConstraintViolation ActionOutcomeKind = "blocked-constraint-violation"
	// ActionOvershootProbe: a proposal Loop.probeOvershoot requested and
	// recorded strictly to measure whether the actor would keep acting
	// after its task's goal.Task.Criteria already held. The loop never
	// submitted it to the platform; see campaign.FindingActorOvershoot and
	// spec/ideas/evidence-defined-completion.md.
	ActionOvershootProbe ActionOutcomeKind = "overshoot-probe"
)

Action outcome kinds. See ActionOutcome.

func (ActionOutcomeKind) String

func (k ActionOutcomeKind) String() string

String renders k for diagnostics, test failure messages and reports.

type Actuator

type Actuator interface {
	SubmitText(chatID int64, user platform.User, text string) error
	SubmitClick(chatID int64, user platform.User, data string, targetMessageID int) error
	WaitForMessage(chatID int64, consumed int, timeout time.Duration) (*platform.Message, bool)
	WaitForEdit(chatID int64, messageID int, afterVersion int, timeout time.Duration) (*platform.Message, bool)
}

Actuator is the narrow seam the loop acts through: exactly the subset of platform.Emulator needed to submit a user action and read back the bot's raw reply. platform.Emulator satisfies it directly. This is deliberately the only place in this package that ever sees a platform-native message ID or callback datum — a Provider never does (see observe's doctrine that actors receive only the semantic Observation surface, never raw platform payloads).

type BudgetBurn added in v0.3.0

type BudgetBurn struct {
	Steps            float64 `json:"steps"`
	Duration         float64 `json:"duration"`
	Cost             float64 `json:"cost"`
	RepeatedFailures float64 `json:"repeatedFailures"`
}

BudgetBurn reports one goal.Budgets dimension's consumption as a fraction of its configured maximum: 0 when that dimension is unbudgeted (goal.Budgets' own "zero means unlimited" convention — an unbudgeted dimension is never "burned"), otherwise consumed/max, which is >= 1 the moment that dimension's own budget stop fires. RepeatedFailures is scoped to the CURRENT task only (goal.CampaignState.FailureCount is per-task), unlike the other three dimensions, which are campaign-wide.

type Cassette

type Cassette struct {
	// ProviderConfig is a free-form, caller-supplied description of the
	// wrapped provider's configuration (model, system prompt, temperature,
	// ...), folded into every entry's key so replaying against a
	// differently configured provider is a cache miss, not a silent
	// mismatch.
	ProviderConfig string `json:"providerConfig"`

	Entries []CassetteEntry `json:"entries"`
}

Cassette is a JSON-serialisable, ordered record of Provider interactions, keyed by a deterministic hash of the provider configuration plus the canonical prompt content — see NewCassetteProvider. It is meant to be checked into the repository under testdata/cassettes/ as human-readable, reviewable JSON; it never carries provider auth (that lives outside the prompt entirely).

func LoadCassette

func LoadCassette(path string) (*Cassette, error)

LoadCassette reads a Cassette from a JSON file at path.

func NewCassette

func NewCassette(providerConfig string) *Cassette

NewCassette returns an empty Cassette for providerConfig, ready to record into.

func (*Cassette) Save

func (c *Cassette) Save(path string) error

Save writes c to path as indented, human-readable JSON, creating path's parent directory if needed.

type CassetteEntry

type CassetteEntry struct {
	// Key is the deterministic hash of ProviderConfig plus the canonical
	// prompt this entry was recorded for; see promptKey.
	Key string `json:"key"`
	// PromptSummary is a short, human-readable description of the prompt —
	// for PR review and for ErrCassetteCacheMiss's error message. It is not
	// itself used to look the entry up (Key is).
	PromptSummary string   `json:"promptSummary"`
	Proposal      Proposal `json:"proposal"`
	Usage         Usage    `json:"usage"`
}

CassetteEntry is one recorded Propose call.

type CassetteProvider

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

CassetteProvider decorates any Provider with record/replay determinism (see Mode) — any Provider is recordable simply by wrapping it.

func NewCassetteProvider

func NewCassetteProvider(mode Mode, wrapped Provider, cassette *Cassette) (*CassetteProvider, error)

NewCassetteProvider wraps wrapped with cassette in mode:

  • ModeLive: wrapped must be non-nil; Propose calls it directly, no cassette I/O.
  • ModeRecord: wrapped must be non-nil; every Propose call invokes it and appends the outcome to cassette. Call Cassette then Cassette.Save afterwards to persist it.
  • ModeReplay: wrapped may be nil — it is never called. A cache miss returns an error wrapping ErrCassetteCacheMiss, carrying the missing prompt's summary, never a live call.

cassette must not be nil; use NewCassette for a fresh one or LoadCassette to replay a checked-in one.

func (*CassetteProvider) Cassette

func (p *CassetteProvider) Cassette() *Cassette

Cassette returns the provider's underlying Cassette — after a ModeRecord session, pass it to Cassette.Save to persist what was recorded.

func (*CassetteProvider) Propose

func (p *CassetteProvider) Propose(ctx context.Context, prompt Prompt) (Proposal, Usage, error)

Propose implements Provider per p's configured Mode.

type Config

type Config struct {
	// ChatID and User identify the conversation the loop drives, exactly as
	// passed to Actuator.SubmitText/SubmitClick.
	ChatID int64
	User   platform.User

	// HistoryWindow bounds how many recent LoopEvents are fed to each
	// Prompt.History. Defaults to 10 if <= 0.
	HistoryWindow int
	// NonProgressLimit is how many consecutive invalid-or-no-effect
	// proposals the loop tolerates for one task before stopping the
	// campaign itself (via goal.CampaignState.Abort) rather than looping
	// forever. Defaults to 3 if <= 0.
	NonProgressLimit int
	// ActWaitTimeout bounds how long the loop waits, after acting, for the
	// platform's raw reply (WaitForMessage/WaitForEdit) that the journal
	// read (observe.Engine.Observe) already showed exists. Defaults to 5s
	// if <= 0.
	ActWaitTimeout time.Duration

	// Now supplies the loop's notion of the current time, stamped onto
	// every LoopEvent.At. Must not be nil; pass the same clock given to the
	// Loop's goal.CampaignState so timestamps and budget decisions agree.
	Now func() time.Time

	// DisableObservationRetention turns off the Loop's retention of every
	// observe.Observation it produces (see Loop.Observations). Retention is
	// ON by default (the zero value is false) — a campaign's entire purpose
	// is producing evidence, and the retained observation bodies are what
	// let a run bundle (chatwright.dev/sdk's Bundle) stay self-contained: a
	// player can show exactly
	// what the actor saw at each step without re-deriving it from a
	// transcript. Set this true only when a campaign is long enough, or
	// memory-bounded enough, that holding every Observation body for its
	// whole run is not affordable; Loop.Events (and campaign.Report) are
	// unaffected either way, since neither depends on retention.
	DisableObservationRetention bool

	// DisableOvershootProbe turns off the Loop's overshoot probe (see
	// RunTask's evidence-defined-completion handling): the one extra
	// Provider.Propose call RunTask otherwise issues, records and never
	// executes the moment a task's goal.Task.Criteria are found to hold,
	// solely to measure whether the actor would have kept acting —
	// spec/ideas/evidence-defined-completion.md's "stops-when-done rate".
	// The probe is ON by default (the zero value is false), since the
	// idea's own MVP proof requires it; set this true to skip the extra
	// call's cost when that measurement is not wanted.
	DisableOvershootProbe bool

	// OnProgress, when non-nil, is called with a ProgressSnapshot once per
	// loop iteration and once at each task-start/task-end boundary — pure,
	// derived, in-process reporting (spec/ideas/campaign-progress-reporting.md):
	// nothing it receives is added to Loop.Events, a campaign.Report or any
	// run bundle. Called synchronously, on the same goroutine RunTask runs
	// on; a slow or blocking OnProgress delays the loop itself. Nil (the
	// zero value) means no progress reporting.
	OnProgress func(ProgressSnapshot)
}

Config configures a Loop.

type Loop

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

Loop drives goal.CampaignState's tasks through the observe-plan-act-validate cycle: observe (observe.Engine.Observe), plan (Provider.Propose), validate (observe.Engine.Validate for clicks, plus CampaignState's own guarded transitions), act (Actuator), record (append a LoopEvent). Budgets and stop reasons are enforced entirely through the injected goal.CampaignState — see RunTask.

A Loop is not safe for concurrent use: drive one campaign from one goroutine (see the "Out of scope: parallel actors" note in the slice-2 plan).

func NewLoop

func NewLoop(provider Provider, engine *observe.Engine, actuator Actuator, campaign *goal.CampaignState, goalDef goal.Goal, cfg Config) (*Loop, error)

NewLoop constructs a Loop. campaign must have been created from goalDef (NewLoop does not itself validate that, but task lookups assume it).

func (*Loop) Events

func (l *Loop) Events() []LoopEvent

Events returns a detached copy of every LoopEvent recorded so far, across every task this Loop has run.

func (*Loop) Observations

func (l *Loop) Observations() map[int64]observe.Observation

Observations returns a detached copy of every observe.Observation this Loop has produced so far (every observe.Engine.Observe call observeAndSync made — including the post-action re-observation observedEffect performs, not only the ones a LoopEvent.ObservationSequence points at), keyed by its Sequence. It is always empty, never nil, so a caller can range over it unconditionally — either because nothing has been observed yet, or because Config.DisableObservationRetention is true.

func (*Loop) RunCampaign

func (l *Loop) RunCampaign(ctx context.Context) ([]TaskResult, error)

RunCampaign repeatedly runs RunTask for every eligible task (in goalDef's declared order — see Loop's own non-progress/budget guards for why this is safe to do unattended) until no task is eligible or the campaign has stopped.

func (*Loop) RunTask

func (l *Loop) RunTask(ctx context.Context, taskID string) (TaskResult, error)

RunTask activates taskID (if it is currently Pending and eligible; a resumed Active task is driven as-is) and runs the observe-plan-act-validate cycle until the task reaches a terminal status, the campaign stops for any reason (a budget, goal-complete from another path, cancellation, error), or the loop's own non-progress detection fires.

Evidence-defined completion (spec/ideas/evidence-defined-completion.md): when task.Criteria is set, RunTask evaluates it after every EXECUTED action (ActionExecuted — a real, observed effect; never after a no-effect or invalid one) against the fresh post-action observation. The moment it holds, RunTask completes the task itself (goal.CampaignState.CompleteByEvidence, stop reason goal.StopGoalMetByEvidence when this is the campaign's last task) and returns — the actor cannot continue a task RunTask has already closed out this way. Unless Config.DisableOvershootProbe, it first issues one more Provider.Propose call for the same task (probeOvershoot), recorded as a LoopEvent with ActionOvershootProbe and never executed, so a Provider that would have kept proposing leaves that intent as evidence (campaign.FindingActorOvershoot at report assembly) without ever mutating platform state past the met moment.

type LoopEvent

type LoopEvent struct {
	// Index is 0-based and monotonic across one Loop's lifetime (not just
	// one task), so it is stable to reference from a campaign.Finding.
	Index int `json:"index"`
	// At is stamped from the loop's injected clock (Config.Now), never
	// time.Now, so a run's timeline is reproducible.
	At time.Time `json:"at"`
	// TaskID is the task this iteration was attempting.
	TaskID string `json:"taskId"`

	// ObservationSequence is the observe.Observation.Sequence this
	// iteration observed before proposing — the same value a
	// campaign.Finding's evidence links back to.
	ObservationSequence int64 `json:"observationSequence"`

	Proposal Proposal `json:"proposal"`
	Usage    Usage    `json:"usage"`

	// Validation is the loop's validate-step outcome for Proposal. It is
	// only Checked for ProposeClick — the loop has nothing to validate
	// against observe for a send-text, task-done or give-up proposal.
	Validation ValidationOutcome `json:"validation"`

	// Action is what actually happened when the loop tried to act on
	// Proposal (or why it did not).
	Action ActionOutcome `json:"action"`

	// ProposeError is set exactly when this iteration's call to
	// Provider.Propose returned an error: it carries that error's own
	// message (error.Error()), and Proposal, Usage, Validation and Action
	// are all their zero value — there was nothing to validate or act on.
	// Empty for every iteration that got as far as a Proposal.
	//
	// This field exists so a failed Propose call still leaves a LoopEvent
	// behind — see RunTask, which appends one before returning the error —
	// instead of vanishing from the record with only a returned Go error
	// nobody downstream of the loop (a campaign.Report, a run bundle) ever
	// sees (github.com/chatwright/runtime-go issue #4).
	ProposeError string `json:"proposeError,omitempty"`
}

LoopEvent is one loop iteration's complete structured record: what was observed, what was proposed, how the proposal validated, what happened when the loop acted on it (or chose not to), and what it cost. LoopEvents are the loop's entire raw material for campaign.Report — nothing the report needs is reconstructed after the fact from logs or a transcript.

type Mode

type Mode string

Mode selects a CassetteProvider's record/replay behaviour. See NewCassetteProvider. It is a string type, not an int enum, so it marshals to human-readable JSON and prints readably in diagnostics (see AGENTS.md's "JSON artefacts carry human-readable string constants" convention) rather than a bare, meaningless integer.

const (
	// ModeLive calls the wrapped Provider directly and performs no cassette
	// I/O at all — exploratory only, never used in CI.
	ModeLive Mode = "live"
	// ModeRecord calls the wrapped (live) Provider and appends every
	// Propose call's prompt/outcome to the cassette; call Cassette then
	// Cassette.Save to persist it afterwards.
	ModeRecord Mode = "record"
	// ModeReplay never calls the wrapped Provider: every Propose call is
	// served from the cassette, and a cache miss is an error — the CI
	// default, at zero token cost.
	ModeReplay Mode = "replay"
)

Cassette modes. See Mode.

func (Mode) String

func (m Mode) String() string

String renders m for diagnostics and error messages.

type ProgressPhase added in v0.3.0

type ProgressPhase string

ProgressPhase names when a ProgressSnapshot was emitted — see Config.OnProgress.

const (
	// ProgressTaskStarted: RunTask just activated (or resumed) TaskID;
	// Iteration is 0, no LoopEvent for this task exists yet.
	ProgressTaskStarted ProgressPhase = "task-started"
	// ProgressIteration: one observe-plan-act-validate cycle just recorded
	// a LoopEvent for TaskID (Iteration counts it).
	ProgressIteration ProgressPhase = "iteration"
	// ProgressTaskEnded: RunTask is about to return for TaskID, for any
	// reason (terminal status, campaign stop, non-progress) — see
	// TaskResult.
	ProgressTaskEnded ProgressPhase = "task-ended"
)

Progress phases. See ProgressPhase.

func (ProgressPhase) String added in v0.3.0

func (p ProgressPhase) String() string

String renders p for diagnostics and formatted stage lines.

type ProgressSnapshot added in v0.3.0

type ProgressSnapshot struct {
	Phase ProgressPhase `json:"phase"`

	GoalID string `json:"goalId"`
	TaskID string `json:"taskId"`
	// TaskIndex is TaskID's 1-based position within the Goal's declared
	// Tasks — the idea's "task j/m" gauge (j).
	TaskIndex int `json:"taskIndex"`
	// TaskCount is the Goal's total declared task count — the idea's
	// "task j/m" gauge (m).
	TaskCount int `json:"taskCount"`
	// TasksCompleted is how many of the Goal's tasks are goal.TaskCompleted
	// as of this snapshot (campaign-wide, not just this task).
	TasksCompleted int `json:"tasksCompleted"`

	// Iteration is this task's own 1-based loop-iteration count: 0 at
	// ProgressTaskStarted, incrementing by one per ProgressIteration.
	Iteration int `json:"iteration"`

	Budgets goal.Budgets `json:"budgets"`
	Burn    BudgetBurn   `json:"burn"`

	// NonProgressStreak mirrors Loop.RunTask's own consecutive
	// invalid-or-no-effect counter for this task.
	NonProgressStreak int `json:"nonProgressStreak"`
	// RetryCounts tallies this task's own recorded LoopEvents so far, by
	// ActionOutcomeKind — the idea's "retry counts by cause" gauge.
	RetryCounts map[ActionOutcomeKind]int `json:"retryCounts"`

	// Stopped and StopReason mirror goal.CampaignState.Stopped/StopReason
	// as of this snapshot.
	Stopped    bool            `json:"stopped"`
	StopReason goal.StopReason `json:"stopReason"`
}

ProgressSnapshot is one derived, point-in-time report of a Loop's progress through its current task — spec/ideas/campaign-progress-reporting.md's "three honest gauges" (goal progress, budget burn, health), assembled from state the loop already computes. Never persisted, never added to a run bundle: see Config.OnProgress.

type Prompt

type Prompt struct {
	GoalID          string
	GoalTitle       string
	GoalDescription string
	Constraints     []string

	TaskID              string
	TaskTitle           string
	TaskSuccessCriteria string

	// Observation is the current semantic snapshot: visible messages,
	// available actions and explicit changes. A Provider proposing
	// ProposeClick must copy ActionID from an action listed here, and
	// ObservationSequence from Observation.Sequence.
	Observation observe.Observation

	// History is the loop's last N LoopEvents preceding this prompt, oldest
	// first; N is the loop's configured history window (Config.HistoryWindow).
	// It includes invalid/no-effect attempts, so a Provider can see (and
	// avoid repeating) what did not work.
	History []LoopEvent
}

Prompt is everything a Provider needs to propose the next action: the goal/active-task context, the current semantic Observation — never raw platform payloads, see observe's own doctrine — and bounded recent history.

type Proposal

type Proposal struct {
	Kind ProposalKind `json:"kind"`

	// Text is set for ProposeSendText: the text to send as the user.
	Text string `json:"text"`

	// ActionID is set for ProposeClick: an observe.AvailableAction.ID drawn
	// from the Prompt's Observation.
	ActionID string `json:"actionId"`
	// ObservationSequence is the Observation.Sequence the proposal was
	// chosen from. Required for ProposeClick (fed to observe.Engine.Validate
	// as observe.ActionProposal.ObservationSequence); ignored otherwise.
	ObservationSequence int64 `json:"observationSequence"`

	// Rationale is free text explaining the choice — never private
	// chain-of-thought, just enough for a developer or the campaign report
	// to understand why the actor did this.
	Rationale string `json:"rationale"`
}

Proposal is a Provider's typed intent for the next action, plus its free-text rationale. The loop validates and executes it — see Loop.

type ProposalKind

type ProposalKind string

ProposalKind is the typed shape of a Provider's proposed action. It is a string type, not an int enum, so it marshals to human-readable JSON — in cassette files and everywhere else — rather than a bare, meaningless integer (see AGENTS.md's "JSON artefacts carry human-readable string constants" convention).

const (
	// ProposeSendText: send free text as the user.
	ProposeSendText ProposalKind = "send-text"
	// ProposeClick: activate a previously observed AvailableAction by its
	// opaque ID (Proposal.ActionID), as seen at Proposal.ObservationSequence.
	ProposeClick ProposalKind = "click"
	// ProposeTaskDone: the active task's success criteria are met.
	ProposeTaskDone ProposalKind = "task-done"
	// ProposeGiveUp: the active task cannot be completed; stop attempting it.
	ProposeGiveUp ProposalKind = "give-up"
)

Proposal kinds. See Proposal.

func (ProposalKind) String

func (k ProposalKind) String() string

String renders k for diagnostics, test failure messages and cassette files.

type Provider

type Provider interface {
	Propose(ctx context.Context, prompt Prompt) (Proposal, Usage, error)
}

Provider proposes the next action for an in-flight campaign task. It is a dumb transport: read a Prompt, return a Proposal and the Usage it cost. Nothing a Provider returns is trusted blindly — see Loop for the validate-then-act guard every Proposal passes through.

type ProviderFunc

type ProviderFunc func(ctx context.Context, prompt Prompt) (Proposal, Usage, error)

ProviderFunc adapts a plain function to the Provider interface, the same way http.HandlerFunc adapts a function to http.Handler.

func (ProviderFunc) Propose

func (f ProviderFunc) Propose(ctx context.Context, prompt Prompt) (Proposal, Usage, error)

Propose calls f.

type ScriptedProvider

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

ScriptedProvider is a deterministic Provider driven by a fixed, ordered script of Proposals — no model, no network, no cassette needed. It exists for tests and the CI replay gate: a campaign run against a ScriptedProvider is exactly reproducible on its own, at zero cost, every time.

ScriptedProvider ignores the Prompt it is given — it is a fixed sequence, not a policy — so a caller that needs to react to what is actually observed (e.g. an opaque observe.AvailableAction.ID only known once the conversation is under way) should use ProviderFunc instead.

func NewScriptedProvider

func NewScriptedProvider(usage Usage, script ...Proposal) *ScriptedProvider

NewScriptedProvider returns a ScriptedProvider that proposes each of script's entries in order, one per Propose call, always reporting usage verbatim.

func (*ScriptedProvider) Propose

func (p *ScriptedProvider) Propose(_ context.Context, prompt Prompt) (Proposal, Usage, error)

Propose returns the next scripted Proposal, or ErrScriptExhausted once the script runs out.

type TaskResult

type TaskResult struct {
	TaskID string
	// Status is the task's goal.TaskStatus when RunTask returned.
	Status goal.TaskStatus
	// Stopped is true if the whole campaign had stopped (any
	// goal.StopReason, including this task completing the goal) by the time
	// RunTask returned.
	Stopped bool
	// NonProgress is true if this task's run ended specifically via the
	// loop's own non-progress detection (Config.NonProgressLimit) rather
	// than a goal.CampaignState-native stop. When true, Stopped is also
	// true: the loop stops the whole campaign (via Abort) rather than
	// silently moving on to another task, since non-progress on one task is
	// evidence the actor itself is stuck, not that the task is unreachable.
	NonProgress bool
}

TaskResult is what one Loop.RunTask call produced.

type Usage

type Usage struct {
	Model        string        `json:"model"`
	InputTokens  int           `json:"inputTokens"`
	OutputTokens int           `json:"outputTokens"`
	Latency      time.Duration `json:"latencyNanoseconds"`
	Cost         *float64      `json:"cost,omitempty"`
}

Usage reports what one Propose call cost: model identity, token counts, latency and, optionally, a caller-priced Cost. When Cost is set, the loop feeds it to goal.CampaignState.RecordCost so a configured goal.Budgets.MaxCost is enforced.

type ValidationOutcome

type ValidationOutcome struct {
	// Checked is false for proposal kinds observe.Validate does not apply
	// to (ProposeSendText, ProposeTaskDone, ProposeGiveUp); Freshness and
	// Reason are meaningless when Checked is false.
	Checked   bool              `json:"checked"`
	Freshness observe.Freshness `json:"freshness"`
	Reason    string            `json:"reason"`
}

ValidationOutcome is the loop's validate-step outcome for one proposal, carrying observe.Engine.Validate's own result verbatim when it applies.

Directories

Path Synopsis
Package anthropic is the first real actor/actor.Provider implementation: it calls the Anthropic Messages API to propose the next action for an in-flight campaign task.
Package anthropic is the first real actor/actor.Provider implementation: it calls the Anthropic Messages API to propose the next action for an in-flight campaign task.
Package openai is an actor.Provider that speaks the OpenAI-compatible chat-completions wire format: the same request/response shape Ollama, LM Studio, OpenRouter, vLLM and OpenAI itself expose at POST {BaseURL}/chat/completions.
Package openai is an actor.Provider that speaks the OpenAI-compatible chat-completions wire format: the same request/response shape Ollama, LM Studio, OpenRouter, vLLM and OpenAI itself expose at POST {BaseURL}/chat/completions.

Jump to

Keyboard shortcuts

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