Documentation
¶
Overview ¶
Package workflow defines types and logic for PromptPack workflow state machines (RFC 0005).
A workflow is an event-driven state machine layered over a PromptPack's prompts. Each state references a prompt_task and defines transitions via named events.
Index ¶
- Constants
- Variables
- func ArtifactMode(def *ArtifactDef) string
- func BuildArtifactToolDescriptor(spec *Spec) *tools.ToolDescriptor
- func BuildTransitionToolDescriptor(events []string) *tools.ToolDescriptor
- func CheckTransition(ctx context.Context, a TransitionAuthorizer, sm *StateMachine, spec *Spec, ...) error
- func HoldsFloor(s *State) bool
- func IsTerminal(s *State) bool
- func MaxVisitsOf(s *State) int
- func OrchestrationOf(s *State) string
- func RegisterArtifactTool(registry *tools.Registry, spec *Spec)
- func RegisterTransitionTool(registry *tools.Registry, state *State)
- func SortedEvents(onEvent map[string]string) []string
- type ArtifactDef
- type ArtifactExecutor
- type ArtifactSnapshot
- type Budget
- type BudgetExhaustedError
- type Context
- func (ctx *Context) Clone() *Context
- func (ctx *Context) GetArtifact(name string) string
- func (ctx *Context) IncrementToolCalls(n int)
- func (ctx *Context) LastTransition() *StateTransition
- func (ctx *Context) RecordTransition(from, to, event string, ts time.Time)
- func (ctx *Context) SetArtifact(name, value, mode string)
- func (ctx *Context) TotalVisits() int
- func (ctx *Context) TransitionCount() int
- type MaxVisitsExceededError
- type Orchestration
- type PendingTransition
- type Persistence
- type Spec
- type State
- type StateMachine
- func (sm *StateMachine) Artifacts() map[string]string
- func (sm *StateMachine) AvailableEvents() []string
- func (sm *StateMachine) Context() *Context
- func (sm *StateMachine) CurrentPromptTask() string
- func (sm *StateMachine) CurrentState() string
- func (sm *StateMachine) IncrementToolCalls(n int)
- func (sm *StateMachine) IsTerminal() bool
- func (sm *StateMachine) ProcessEvent(event string) (*TransitionResult, error)
- func (sm *StateMachine) SetArtifact(name, value string)
- func (sm *StateMachine) WithTimeFunc(fn TimeFunc) *StateMachine
- type StateTransition
- type TimeFunc
- type TransitionAuthorizer
- type TransitionExecutor
- func (e *TransitionExecutor) ClearPending()
- func (e *TransitionExecutor) CommitPending() (*TransitionResult, error)
- func (e *TransitionExecutor) Execute(ctx context.Context, _ *tools.ToolDescriptor, args json.RawMessage) (json.RawMessage, error)
- func (e *TransitionExecutor) Name() string
- func (e *TransitionExecutor) Pending() *PendingTransition
- func (e *TransitionExecutor) RegisterForState(registry *tools.Registry, state *State)
- func (e *TransitionExecutor) SetAuthorizer(a TransitionAuthorizer)
- func (e *TransitionExecutor) SetOnCommit(fn func(*TransitionResult))
- func (e *TransitionExecutor) SetOnCommitError(fn func(event string, err error))
- func (e *TransitionExecutor) StateMachine() *StateMachine
- type TransitionRequest
- type TransitionResult
- type ValidationResult
Constants ¶
const ( BudgetLimitTotalVisits = "max_total_visits" BudgetLimitToolCalls = "max_tool_calls" BudgetLimitWallTimeSec = "max_wall_time_sec" )
Budget limit names, used by BudgetExhaustedError.Limit.
const ( ArtifactModeReplace = "replace" ArtifactModeAppend = "append" )
Artifact merge modes (schema $defs/ArtifactDef.mode).
const ( PersistenceTransient = "transient" PersistencePersistent = "persistent" )
Persistence values.
const ( OrchestrationInternal = "internal" OrchestrationExternal = "external" OrchestrationHybrid = "hybrid" // OrchestrationComposition runs a declarative composition step-graph for the // state instead of an LLM-driven turn (RFC 0010). OrchestrationComposition = "composition" )
Orchestration values.
const ( // ControlUser yields the conversation to the user on entering the state. ControlUser = "user" // ControlAgent runs another agent round in the state without yielding, // for transient routing and processing states. ControlAgent = "agent" )
Turn control values (schema $defs/WorkflowState.control, RFC 0014).
Orthogonal to Orchestration: that declares who INITIATES a transition, this declares who holds the turn AFTER one. Read a state's value through HoldsFloor, which documents how an absent value resolves here.
const ArtifactExecutorMode = "workflow-artifact"
ArtifactExecutorMode is the executor name for Mode-based routing.
const ArtifactToolName = "workflow__set_artifact"
ArtifactToolName is the qualified name of the workflow set_artifact tool.
const MaxHistoryLength = 1000
MaxHistoryLength is the maximum number of state transitions retained in context history. When exceeded, the oldest transitions are discarded.
const TransitionExecutorMode = "workflow-transition"
TransitionExecutorMode is the executor name used for Mode-based routing.
const TransitionToolName = "workflow__transition"
TransitionToolName is the qualified name of the workflow transition tool.
Variables ¶
var ( // ErrInvalidEvent is returned when an event is not defined for the current state. ErrInvalidEvent = errors.New("invalid event for current state") // ErrTerminalState is returned when trying to process an event in a terminal state. ErrTerminalState = errors.New("current state is terminal (no outgoing transitions)") )
var ( // ErrMaxVisitsExceeded is returned when a state's max_visits limit is reached // and no on_max_visits fallback is configured. The concrete error returned // is typically a *MaxVisitsExceededError wrapping this sentinel; callers // wanting structured details should use errors.As. ErrMaxVisitsExceeded = errors.New("max visits exceeded") // ErrBudgetExhausted is returned when a workflow-level budget limit is // reached. The concrete error returned is typically a *BudgetExhaustedError // wrapping this sentinel; callers wanting structured details should use // errors.As. ErrBudgetExhausted = errors.New("workflow budget exhausted") )
Sentinel errors for workflow execution.
Functions ¶
func ArtifactMode ¶
func ArtifactMode(def *ArtifactDef) string
ArtifactMode returns an artifact slot's merge mode, applying the spec default of "replace" when the pack did not set one.
func BuildArtifactToolDescriptor ¶
func BuildArtifactToolDescriptor(spec *Spec) *tools.ToolDescriptor
BuildArtifactToolDescriptor creates a tools.ToolDescriptor for the workflow__set_artifact tool, with artifact names as an enum constraint.
func BuildTransitionToolDescriptor ¶
func BuildTransitionToolDescriptor(events []string) *tools.ToolDescriptor
BuildTransitionToolDescriptor creates a tools.ToolDescriptor for the workflow__transition tool with the given events as an enum constraint.
func CheckTransition ¶ added in v2.13.0
func CheckTransition( ctx context.Context, a TransitionAuthorizer, sm *StateMachine, spec *Spec, event string, ) error
CheckTransition asks a whether event may fire from sm's current state.
It returns nil when a is nil, and when event does not resolve from the current state: such a transition fails in ProcessEvent on its own, so there is nothing for a host to authorize.
func HoldsFloor ¶
HoldsFloor reports whether the agent keeps the turn on entering s, rather than yielding the conversation to the user (RFC 0014's `control`).
True means the turn runs on: the tool loop swaps to this state's prompt and tools and takes another round, so a routing or processing state can speak without waiting for a user message.
An absent control does NOT resolve to "user" here, and that is deliberate ¶
RFC 0014 defaults the field to "user" and describes that as "the behavior of every state before v1.7.0". That is not true of this runtime. In-turn state handoff shipped 2026-08-14 (#1747); RFC 0014 arrived with the v1.7.0 schema on 2026-08-31, seventeen days later. Before it the spec said nothing about who holds the turn after a transition — there was no default to deviate from — and this runtime already ran the destination state, which is what lets a pack route through several states and produce one reply.
Honoring the RFC default would silently invert that for every pack written before it, turning routing states into dead stops. So a DECLARED control is honored exactly as specified, and an absent one keeps the behavior packs already rely on.
This is a permanent, deliberate divergence, confined to what absent means. It is not an oversight and not a migration step, so do not "fix" it back to the spec default: doing so changes the behavior of every pack that never declares the field, which is all of them written before v1.7.0.
The RFC's claim that "user" was the behavior of every state before v1.7.0 is what this contradicts, and correcting that wording is AltairaLabs/promptpack-spec#79. The default itself is not in dispute.
A nil state yields: there is nothing to run on into.
func IsTerminal ¶
IsTerminal reports whether a state ends the workflow.
terminal is *bool on the generated type because the spec makes it optional, and absent means "not terminal" rather than "unset". A state with no outgoing events is terminal regardless of the flag.
func MaxVisitsOf ¶
MaxVisitsOf returns a state's visit cap, or 0 when uncapped.
func OrchestrationOf ¶
OrchestrationOf returns a state's control mode, defaulting to OrchestrationInternal when the state does not declare one.
func RegisterArtifactTool ¶
RegisterArtifactTool registers the workflow__set_artifact tool in the given registry. Only registers if the spec declares artifacts on any state.
func RegisterTransitionTool ¶
RegisterTransitionTool registers the workflow__transition tool in the given registry for the specified state's available events. Skips terminal states (no events) and externally orchestrated states.
Both the SDK (WorkflowCapability) and Arena (engine) use this function.
func SortedEvents ¶
SortedEvents returns a sorted copy of the event keys from an OnEvent map.
Types ¶
type ArtifactDef ¶
type ArtifactDef = packspec.ArtifactDef
ArtifactDef declares a named artifact slot on a workflow state.
Generated from the schema: an ALIAS for packspec.ArtifactDef.
Mode is a *string, not a string: the spec defaults it to "replace", so "absent" and "explicitly empty" are different facts and a plain field would collapse them. Read it through ArtifactMode, which applies the default.
type ArtifactExecutor ¶
type ArtifactExecutor struct {
// contains filtered or unexported fields
}
ArtifactExecutor implements tools.Executor for workflow__set_artifact. Unlike TransitionExecutor, artifact mutations are safe to apply immediately since they only modify the context map without closing/reopening conversations.
func NewArtifactExecutor ¶
func NewArtifactExecutor(sm *StateMachine) *ArtifactExecutor
NewArtifactExecutor creates an ArtifactExecutor for the given state machine.
func (*ArtifactExecutor) Execute ¶
func (e *ArtifactExecutor) Execute( _ context.Context, _ *tools.ToolDescriptor, args json.RawMessage, ) (json.RawMessage, error)
Execute implements tools.Executor. Sets the artifact value on the state machine.
func (*ArtifactExecutor) Name ¶
func (e *ArtifactExecutor) Name() string
Name implements tools.Executor.
type ArtifactSnapshot ¶
type ArtifactSnapshot struct {
FromState string `json:"from_state"`
ToState string `json:"to_state"`
Event string `json:"event"`
Values map[string]string `json:"values"`
Timestamp time.Time `json:"timestamp"`
}
ArtifactSnapshot captures artifact values at a specific state transition.
type Budget ¶
type Budget = packspec.WorkflowBudget
Budget defines workflow-level resource limits from the engine block. Generated from the schema: an ALIAS for packspec.WorkflowBudget. Optional limits are pointers so "no limit" is distinct from a limit of zero.
type BudgetExhaustedError ¶
type BudgetExhaustedError struct {
// Limit is one of BudgetLimitTotalVisits, BudgetLimitToolCalls,
// BudgetLimitWallTimeSec.
Limit string
// Current is the observed value at the time the limit was hit.
Current int
// Max is the configured limit.
Max int
// CurrentState is the state the workflow was in when the budget tripped.
CurrentState string
}
BudgetExhaustedError is the structured error returned from ProcessEvent when a workflow-level budget is reached. It wraps ErrBudgetExhausted so errors.Is still matches.
func (*BudgetExhaustedError) Error ¶
func (e *BudgetExhaustedError) Error() string
Error returns a human-readable description.
func (*BudgetExhaustedError) Unwrap ¶
func (e *BudgetExhaustedError) Unwrap() error
Unwrap returns the sentinel for errors.Is.
type Context ¶
type Context struct {
CurrentState string `json:"current_state"`
History []StateTransition `json:"history"`
Metadata map[string]any `json:"metadata,omitempty"`
VisitCounts map[string]int `json:"visit_counts,omitempty"` // RFC 0009: per-state visit counts
TotalToolCalls int `json:"total_tool_calls,omitempty"` // RFC 0009: workflow-wide tool call count
Artifacts map[string]string `json:"artifacts,omitempty"` // RFC 0009: current artifact values
ArtifactHistory []ArtifactSnapshot `json:"artifact_history,omitempty"` // RFC 0009: artifact values at each transition
StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Context holds the runtime state of a workflow execution.
func NewContext ¶
NewContext creates a new Context initialized at the given entry state.
func (*Context) Clone ¶
Clone returns a deep copy of the Context. Nil collections on the source are preserved as nil on the clone so callers can distinguish "absent" from "present but empty" across the round-trip.
func (*Context) GetArtifact ¶
GetArtifact returns an artifact value, or empty string if not set.
func (*Context) IncrementToolCalls ¶
IncrementToolCalls adds n to the workflow-wide tool call counter.
func (*Context) LastTransition ¶
func (ctx *Context) LastTransition() *StateTransition
LastTransition returns the most recent transition, or nil if none.
func (*Context) RecordTransition ¶
RecordTransition records a state transition and updates the current state.
func (*Context) SetArtifact ¶
SetArtifact sets an artifact value, respecting the mode (replace or append).
func (*Context) TotalVisits ¶
TotalVisits returns the sum of all per-state visit counts.
func (*Context) TransitionCount ¶
TransitionCount returns the number of transitions recorded.
type MaxVisitsExceededError ¶
type MaxVisitsExceededError struct {
// FromState is the state the transition was leaving.
FromState string
// OriginalTarget is the state whose max_visits was reached.
OriginalTarget string
// Event is the transition event that triggered the attempt.
Event string
// VisitCount is the number of times OriginalTarget had already been entered.
VisitCount int
// MaxVisits is the declared limit on OriginalTarget.
MaxVisits int
}
MaxVisitsExceededError is the structured error returned from ProcessEvent when a state has reached its max_visits cap and no on_max_visits fallback is configured. It wraps ErrMaxVisitsExceeded so errors.Is still matches.
func (*MaxVisitsExceededError) Error ¶
func (e *MaxVisitsExceededError) Error() string
Error returns a human-readable description.
func (*MaxVisitsExceededError) Unwrap ¶
func (e *MaxVisitsExceededError) Unwrap() error
Unwrap returns the sentinel for errors.Is.
type Orchestration ¶
type Orchestration string
Orchestration is the control mode for a workflow state.
type PendingTransition ¶
type PendingTransition struct {
Event string `json:"event"`
ContextSummary string `json:"context"`
HostExtras tools.HostExtras `json:"host_extras,omitempty"`
}
PendingTransition captures a deferred workflow transition from a tool call.
type Spec ¶
type Spec = packspec.WorkflowConfig
Spec is a pack's workflow state-machine specification.
Generated. It was hand-written only because its states map held a hand-written State; it carries no methods of its own.
func ParseConfig ¶
ParseConfig parses an untyped workflow config (typically from config.Workflow which is stored as interface{}) into a typed Spec. Returns nil, nil when raw is nil.
type State ¶
type State = packspec.WorkflowState
State is a single state within a workflow.
Generated. terminal, max_visits and orchestration are pointers on the generated type because the spec makes them optional — which is more correct than the values they replaced, since absent is now distinguishable from the zero. Use IsTerminal, MaxVisitsOf and OrchestrationOf rather than reading them directly; they resolve the documented default in one place.
type StateMachine ¶
type StateMachine struct {
// contains filtered or unexported fields
}
StateMachine manages workflow state transitions.
func NewStateMachine ¶
func NewStateMachine(spec *Spec) *StateMachine
NewStateMachine creates a state machine from a workflow spec. It initializes the context to the entry state.
func NewStateMachineFromContext ¶
func NewStateMachineFromContext(spec *Spec, ctx *Context) *StateMachine
NewStateMachineFromContext restores a state machine from persisted context.
func (*StateMachine) Artifacts ¶
func (sm *StateMachine) Artifacts() map[string]string
Artifacts returns a snapshot of the current artifact values.
func (*StateMachine) AvailableEvents ¶
func (sm *StateMachine) AvailableEvents() []string
AvailableEvents returns the set of valid events for the current state, sorted.
func (*StateMachine) Context ¶
func (sm *StateMachine) Context() *Context
Context returns a snapshot of the current workflow context for persistence.
func (*StateMachine) CurrentPromptTask ¶
func (sm *StateMachine) CurrentPromptTask() string
CurrentPromptTask returns the prompt_task for the current state.
func (*StateMachine) CurrentState ¶
func (sm *StateMachine) CurrentState() string
CurrentState returns the name of the current state.
func (*StateMachine) IncrementToolCalls ¶
func (sm *StateMachine) IncrementToolCalls(n int)
IncrementToolCalls adds n to the workflow-wide tool call counter. Thread-safe; intended to be called by the SDK after tool executions.
func (*StateMachine) IsTerminal ¶
func (sm *StateMachine) IsTerminal() bool
IsTerminal returns true if the current state is terminal. A state is terminal when explicitly marked (Terminal: true) or when it has no outgoing transitions (backward compatible).
func (*StateMachine) ProcessEvent ¶
func (sm *StateMachine) ProcessEvent(event string) (*TransitionResult, error)
ProcessEvent applies an event and transitions to the target state. Returns a TransitionResult describing the transition (including any max_visits redirect). Returns ErrMaxVisitsExceeded when the target state's visit limit is reached and no on_max_visits fallback is set. Returns ErrBudgetExhausted when a workflow-level budget limit is reached.
func (*StateMachine) SetArtifact ¶
func (sm *StateMachine) SetArtifact(name, value string)
SetArtifact sets an artifact value on the workflow context. The mode is looked up from the spec's artifact definitions for the current state. Thread-safe.
func (*StateMachine) WithTimeFunc ¶
func (sm *StateMachine) WithTimeFunc(fn TimeFunc) *StateMachine
WithTimeFunc sets a custom time function for deterministic tests.
type StateTransition ¶
type StateTransition struct {
From string `json:"from"`
To string `json:"to"`
Event string `json:"event"`
Timestamp time.Time `json:"timestamp"`
}
StateTransition records a single state transition.
type TransitionAuthorizer ¶ added in v2.13.0
type TransitionAuthorizer interface {
AuthorizeTransition(ctx context.Context, req TransitionRequest) error
}
TransitionAuthorizer decides whether a workflow transition may happen.
PromptKit calls it before every transition it would make, whether the model requested it through workflow__transition or the host fired the event itself. The host implements its own policy, typically over the two states' declarations and their extensions (RFC 0016). A non-nil error refuses the transition and its text is the reason; PromptKit ships no policy and, with no authorizer configured, allows every transition.
A host-fired transition is authorized while the workflow conversation holds its lock, so an authorizer must not call back into the conversation (its CurrentState, Context, Transition, ...): that deadlocks. Everything it needs about the transition is in the TransitionRequest.
type TransitionExecutor ¶
type TransitionExecutor struct {
// contains filtered or unexported fields
}
TransitionExecutor implements tools.Executor for workflow__transition.
It defers the state transition (ProcessEvent) until CommitPending is called, ensuring the full pipeline turn completes before state changes. The optional OnCommit callback is invoked after a successful commit so consumers (Arena, SDK) can run their post-commit work (re-register tool descriptor, update scenario TaskType, emit observability events) from a single hook.
func NewTransitionExecutor ¶
func NewTransitionExecutor(sm *StateMachine, spec *Spec) *TransitionExecutor
NewTransitionExecutor creates a TransitionExecutor for the given state machine.
func (*TransitionExecutor) ClearPending ¶
func (e *TransitionExecutor) ClearPending()
ClearPending discards any pending transition without committing.
func (*TransitionExecutor) CommitPending ¶
func (e *TransitionExecutor) CommitPending() (*TransitionResult, error)
CommitPending applies the pending transition by calling ProcessEvent. Returns nil, nil if no transition is pending. After commit, the pending state is cleared. Thread-safe. Fires OnCommit on success.
func (*TransitionExecutor) Execute ¶
func (e *TransitionExecutor) Execute( ctx context.Context, _ *tools.ToolDescriptor, args json.RawMessage, ) (json.RawMessage, error)
Execute implements tools.Executor.
The request is stored as pending and CommitPending applies it after the pipeline turn finishes. This is RFC 0005's deferred-commit pattern.
The LLM's `context` argument is stored on the PendingTransition and surfaced to the new conversation as the `workflow_context` template variable when the consumer opens it.
With an authorizer installed, the transition is authorized first. A refusal records nothing and returns a tool result saying so, with the authorizer's reason, so the model stays in the current state and can answer from it.
func (*TransitionExecutor) Name ¶
func (e *TransitionExecutor) Name() string
Name implements tools.Executor. Returns the mode name for registry routing.
func (*TransitionExecutor) Pending ¶
func (e *TransitionExecutor) Pending() *PendingTransition
Pending returns the current pending transition, or nil if none.
func (*TransitionExecutor) RegisterForState ¶
func (e *TransitionExecutor) RegisterForState(registry *tools.Registry, state *State)
RegisterForState registers the workflow__transition tool in the given registry for the specified state, with Mode set for executor routing.
When called with a terminal state (Terminal: true OR no on_event), the previous descriptor — if any — is removed from the registry instead of re-registered. Without this, the LLM would still see workflow__transition in the tool list after entering a terminal state and could call it with the previous state's now-stale events. Externally orchestrated states skip both register and unregister: the caller owns the descriptor's lifecycle.
func (*TransitionExecutor) SetAuthorizer ¶ added in v2.13.0
func (e *TransitionExecutor) SetAuthorizer(a TransitionAuthorizer)
SetAuthorizer installs the host's TransitionAuthorizer, consulted before a requested transition is recorded. Nil (the default) allows every transition.
func (*TransitionExecutor) SetOnCommit ¶
func (e *TransitionExecutor) SetOnCommit(fn func(*TransitionResult))
SetOnCommit registers a callback fired after every successful commit. Pass nil to clear.
Callbacks run while the executor's internal lock is held; they must not re-enter the executor's public methods (Execute / CommitPending / etc.).
func (*TransitionExecutor) SetOnCommitError ¶
func (e *TransitionExecutor) SetOnCommitError(fn func(event string, err error))
SetOnCommitError registers a callback fired when ProcessEvent fails during CommitPending. Consumers wire their workflow observability error emit (e.g., workflow.max_visits_exceeded, workflow.budget_exhausted) through this hook so deferred-commit failures are observable. Pass nil to clear.
Same locking contract as SetOnCommit: the callback runs while the executor's internal lock is held; do not re-enter the executor.
func (*TransitionExecutor) StateMachine ¶
func (e *TransitionExecutor) StateMachine() *StateMachine
StateMachine returns the underlying state machine for metadata access.
type TransitionRequest ¶ added in v2.13.0
type TransitionRequest struct {
// From and To are state names. To is the state the current state's
// on_event names for Event. A max_visits redirect at commit time is the
// state machine's own safety limit and is not re-authorized.
From, To string
// FromState and ToState are copies of the two states' declarations, so
// an authorizer cannot change the loaded workflow through them.
FromState, ToState *packspec.WorkflowState
// Event is the event that fires the transition.
Event string
}
TransitionRequest describes a transition about to happen.
type TransitionResult ¶
type TransitionResult struct {
From string `json:"from"`
To string `json:"to"`
Event string `json:"event"`
Redirected bool `json:"redirected,omitempty"`
RedirectReason string `json:"redirect_reason,omitempty"`
OriginalTarget string `json:"original_target,omitempty"`
HostExtras tools.HostExtras `json:"host_extras,omitempty"`
}
TransitionResult is returned by ProcessEvent to communicate what happened. Redirects (e.g., max_visits exceeded → on_max_visits) are successful transitions, not errors.
type ValidationResult ¶
type ValidationResult struct {
Errors []string // Blocking: invalid references, missing fields
Warnings []string // Non-blocking: PascalCase violations, circular refs
}
ValidationResult holds errors and warnings from workflow validation.
func Validate ¶
func Validate(spec *Spec, promptKeys []string) *ValidationResult
Validate checks a Spec against the available prompt keys. It implements all 10 validation rules from RFC 0005.
func (*ValidationResult) HasErrors ¶
func (r *ValidationResult) HasErrors() bool
HasErrors returns true if there are blocking validation errors.