Files
noema/internal/planning/types.go
T
Andy Dixon 813b067842 feat(planning): plans, validation, planner and a restart-safe executor
Plans turn intentions into revisable dependency graphs (migration 0027):
objective, assumptions, ordered or dependent steps, capabilities,
expected observations, preconditions, risk, estimates, budget,
completion criteria, abort conditions and the alternatives considered.

- Validation owns structure: acyclic dependencies, registered and
  unlocked capabilities only, schema-valid parameters, targets that agree
  with parameters, read-only checks, a closed set of step kinds and
  internal actions. Model proposals are parsed into the same structure;
  fields that could carry authority or claim progress are reported as
  ignored, and an invalid draft is stored as a draft that never runs.
- The planner prefers deterministic strategies: a direct action verified
  by observation, a read-only diagnosis that requests temporary scoped log
  access with risk and fallback stated, and an escalation that diagnoses
  and informs the operator instead of repeating a failed action. Failed
  strategies are not reused; a model is consulted only when none applies.
- The executor holds no state in memory: it claims a step with a
  conditional update, acts only through Invoke and RequestCapability, and
  records the outcome. Waiting for approval or access forms a subordinate
  intention and blocks the parent with the reason. Expectations become
  verification predictions resolved from a read-only observation, so an
  HTTP-level success is never taken as the outcome; a miss is a prediction
  error that weakens the supporting belief and triggers reconsideration.
  Replanning creates a new revision, withdraws open requests of the old
  one and keeps it as history, within a replan limit. Checks never raise
  approval prompts. Budgets stop runaway plans. Preconditions skip actions
  when the world already changed. Dream plans and disabled sandbox minds
  never act. An interrupted non-idempotent action with no record is
  blocked for verification instead of repeated; recorded outcomes are
  adopted. Completed plans are stored as procedural memories; a completed
  escalation does not count as fulfilling the intention.
- Executions and approvals record their origin; minds may withdraw their
  own pending approval requests; Preview reports a decision without side
  effects; service.status is allowed by default as a bounded read.
2026-09-14 23:38:44 +01:00

300 lines
13 KiB
Go

// Package planning turns intentions into explicit, revisable plans and
// carries them out (docs/planning.md). A plan is a dependency graph of steps
// with prerequisites, capabilities, expected observations, risk, resource
// estimates, completion criteria, abort conditions and alternatives. Go code
// owns validation, dependency integrity, the capability vocabulary, policy
// checks (through the capability layer), resource accounting, execution
// state, retries, deadlines and approvals. A language model may propose a
// decomposition; it is parsed into the same structure and validated like any
// other draft, and it can never name a capability that is not registered.
//
// Plans are not promises and not authority. Every action goes through
// capabilities.Service.Invoke, and replanning creates a new revision while
// the old plan stays as history.
package planning
import (
"time"
"github.com/andydixon/noema/pkg/ids"
)
// Plan statuses.
const (
PlanDraft = "draft"
PlanValidated = "validated"
PlanWaiting = "waiting"
PlanExecuting = "executing"
PlanBlocked = "blocked"
PlanAwaitingApproval = "awaiting_approval"
PlanReplanning = "replanning"
PlanCompleted = "completed"
PlanFailed = "failed"
PlanAbandoned = "abandoned"
PlanSuperseded = "superseded"
)
// PlanOpen reports whether a plan may still make progress.
func PlanOpen(status string) bool {
switch status {
case PlanValidated, PlanWaiting, PlanExecuting, PlanBlocked, PlanAwaitingApproval, PlanReplanning:
return true
}
return false
}
// Step kinds.
const (
KindCapability = "capability"
KindRequestCapability = "request_capability"
KindVerify = "verify"
KindWait = "wait"
KindMonitor = "monitor"
KindInternal = "internal"
)
// Step statuses.
const (
StepPending = "pending"
StepReady = "ready"
StepExecuting = "executing"
StepAwaitingApproval = "awaiting_approval"
StepAwaitingGrant = "awaiting_grant"
StepWaiting = "waiting"
StepCompleted = "completed"
StepFailed = "failed"
StepSkipped = "skipped"
StepBlocked = "blocked"
StepCancelled = "cancelled"
)
// StepDone reports whether a step no longer blocks its dependants.
func StepDone(status string) bool { return status == StepCompleted || status == StepSkipped }
// StepTerminal reports whether a step will not change again by itself.
func StepTerminal(status string) bool {
return status == StepCompleted || status == StepSkipped || status == StepFailed || status == StepCancelled
}
// Check observes the world through a read-only capability and compares one
// value of its structured result.
type Check struct {
Capability string `json:"capability"`
Target string `json:"target,omitempty"`
Params map[string]any `json:"params,omitempty"`
Path string `json:"path"` // dot path into the result data, e.g. "active" or "properties.ActiveState"
Op string `json:"op"` // eq | ne | lt | lte | gt | gte | contains | exists
Value any `json:"value,omitempty"` // compared value
Description string `json:"description,omitempty"`
}
// Expectation is what a consequential step should bring about. It becomes a
// prediction when the step completes; a verify step resolves it.
type Expectation struct {
Description string `json:"description"`
Confidence float64 `json:"confidence"`
WithinSeconds int `json:"within_seconds"`
BeliefID ids.ID `json:"belief_id,omitempty"` // belief the expectation rests on; a miss is evidence against it
}
// Precondition must hold before a step acts. When it does not, the step is
// skipped: if SatisfiedIfFalse, because the desired state already holds.
type Precondition struct {
Check Check `json:"check"`
SatisfiedIfFalse bool `json:"satisfied_if_false"`
}
// RequestSpec is a structured capability request a step raises.
type RequestSpec struct {
Capability string `json:"capability"`
TargetPattern string `json:"target_pattern"`
ParamConstraints map[string]string `json:"param_constraints,omitempty"`
DurationSeconds int `json:"duration_seconds"`
MaxOperations *int `json:"max_operations,omitempty"`
Reason string `json:"reason"`
Risk string `json:"risk"`
Mitigation string `json:"mitigation"`
Fallback string `json:"fallback"`
Alternatives []string `json:"alternatives,omitempty"`
}
// VerifySpec resolves another step's expectation by observation.
type VerifySpec struct {
Of string `json:"of"` // key of the step whose expectation is checked
Check Check `json:"check"`
WithinSeconds int `json:"within_seconds"` // keep checking until this long after the action
IntervalSeconds int `json:"interval_seconds"` // between checks
}
// MonitorSpec repeats a check until it holds continuously for StableSeconds,
// or fails at DeadlineSeconds.
type MonitorSpec struct {
Check Check `json:"check"`
IntervalSeconds int `json:"interval_seconds"`
StableSeconds int `json:"stable_seconds"`
DeadlineSeconds int `json:"deadline_seconds"`
}
// WaitSpec pauses a branch.
type WaitSpec struct {
Seconds int `json:"seconds"`
}
// InternalSpec is a deterministic internal step.
type InternalSpec struct {
Action string `json:"action"` // summarise_findings | record_procedure
}
// InternalActions is the closed vocabulary of internal steps.
var InternalActions = map[string]string{
"summarise_findings": "collect the observations of the steps it depends on into one record for belief formation",
"record_procedure": "record the steps that achieved the outcome as a procedural memory",
}
// StepSpec is one step of a draft.
type StepSpec struct {
Key string `json:"key"`
Title string `json:"title"`
Kind string `json:"kind"`
DependsOn []string `json:"depends_on,omitempty"`
Capability string `json:"capability,omitempty"`
Target string `json:"target,omitempty"`
Params map[string]any `json:"params,omitempty"`
RequireApproval bool `json:"require_approval,omitempty"`
MaxAttempts int `json:"max_attempts,omitempty"`
Expectation *Expectation `json:"expectation,omitempty"`
Precondition *Precondition `json:"precondition,omitempty"`
Request *RequestSpec `json:"request,omitempty"`
Verify *VerifySpec `json:"verify,omitempty"`
Monitor *MonitorSpec `json:"monitor,omitempty"`
Wait *WaitSpec `json:"wait,omitempty"`
Internal *InternalSpec `json:"internal,omitempty"`
}
// Assumption is a belief the plan depends on. If the belief falls below
// MinConfidence the plan is reconsidered.
type Assumption struct {
BeliefID ids.ID `json:"belief_id,omitempty"`
Proposition string `json:"proposition"`
MinConfidence float64 `json:"min_confidence,omitempty"`
}
// Resources are estimated or consumed resources.
type Resources struct {
Invocations int `json:"capability_invocations"`
Approvals int `json:"human_approvals"`
WallSeconds float64 `json:"wall_seconds"`
Network int `json:"network_operations"`
Money float64 `json:"money"`
Tokens int `json:"llm_tokens"`
}
// Budget caps a plan's consumption. Zero fields are unlimited except
// Invocations, which defaults to 20 so no plan can loop without bound.
type Budget struct {
MaxInvocations int `json:"max_capability_invocations"`
MaxWallSeconds float64 `json:"max_wall_seconds,omitempty"`
MaxMoney float64 `json:"max_money,omitempty"`
MaxApprovals int `json:"max_human_approvals,omitempty"`
}
// Alternative is a strategy the planner considered.
type Alternative struct {
Strategy string `json:"strategy"`
Description string `json:"description"`
Estimate Resources `json:"estimate"`
ExpectedConfidence float64 `json:"expected_confidence"`
Score float64 `json:"score"`
Selected bool `json:"selected"`
Why string `json:"why,omitempty"`
}
// Draft is a proposed plan before it is stored.
type Draft struct {
Objective string `json:"objective"`
Origin string `json:"origin"` // template | model | operator | dream | replan
Strategy string `json:"strategy"`
Steps []StepSpec `json:"steps"`
Assumptions []Assumption `json:"assumptions,omitempty"`
CompletionCriteria string `json:"completion_criteria"`
AbortConditions []string `json:"abort_conditions,omitempty"`
Alternatives []Alternative `json:"alternatives,omitempty"`
Risk string `json:"risk"`
Budget Budget `json:"budget"`
RequireApproval bool `json:"require_approval,omitempty"`
LLMCall ids.ID `json:"llm_call,omitempty"`
Ignored []string `json:"ignored,omitempty"` // fields of a model proposal that were not accepted
Estimate *Resources `json:"estimate,omitempty"`
}
// Plan is a stored plan.
type Plan struct {
ID ids.ID `json:"id"`
MindID ids.ID `json:"mind_id"`
IntentionID ids.ID `json:"intention_id"`
Revision int `json:"revision"`
Supersedes ids.ID `json:"supersedes,omitempty"`
SupersededBy ids.ID `json:"superseded_by,omitempty"`
Objective string `json:"objective"`
Origin string `json:"origin"`
Strategy string `json:"strategy"`
Status string `json:"status"`
Assumptions []Assumption `json:"assumptions"`
CompletionCriteria string `json:"completion_criteria"`
AbortConditions []string `json:"abort_conditions"`
Alternatives []Alternative `json:"alternatives"`
Risk string `json:"risk"`
Estimate Resources `json:"estimate"`
Actual Resources `json:"actual"`
Budget Budget `json:"budget"`
Validation Validation `json:"validation"`
RequireApproval bool `json:"require_approval"`
MaxReplans int `json:"max_replans"`
LLMCall ids.ID `json:"llm_call,omitempty"`
StatusReason string `json:"status_reason"`
ClosedAt *time.Time `json:"closed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Steps []Step `json:"steps,omitempty"`
}
// Step is a stored step with its lifecycle state.
type Step struct {
ID ids.ID `json:"id"`
PlanID ids.ID `json:"plan_id"`
MindID ids.ID `json:"mind_id"`
Key string `json:"key"`
Position int `json:"position"`
Title string `json:"title"`
Kind string `json:"kind"`
DependsOn []string `json:"depends_on"`
Capability string `json:"capability"`
Target string `json:"target"`
Params map[string]any `json:"params"`
RequireApproval bool `json:"require_approval"`
Expectation *Expectation `json:"expectation,omitempty"`
Precondition *Precondition `json:"precondition,omitempty"`
Spec StepSpec `json:"spec"`
Status string `json:"status"`
Attempts int `json:"attempts"`
MaxAttempts int `json:"max_attempts"`
LeaseUntil *time.Time `json:"lease_until,omitempty"`
NextRunAt *time.Time `json:"next_run_at,omitempty"`
ApprovalID ids.ID `json:"approval_id,omitempty"`
RequestID ids.ID `json:"request_id,omitempty"`
ExecutionID ids.ID `json:"execution_id,omitempty"`
PredictionID ids.ID `json:"prediction_id,omitempty"`
IntentionID ids.ID `json:"intention_id,omitempty"`
Result map[string]any `json:"result,omitempty"`
Observation map[string]any `json:"observation,omitempty"`
Error string `json:"error"`
StatusReason string `json:"status_reason"`
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Origin returns the origin string recorded on invocations for this step.
func (s Step) Origin() string { return "plan_step:" + string(s.ID) }