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.
This commit is contained in:
Andy Dixon
2026-09-14 23:38:44 +01:00
parent 9db188ffe6
commit 813b067842
14 changed files with 3464 additions and 20 deletions
+8 -3
View File
@@ -760,7 +760,9 @@ type Trigger struct {
Action *ActionInfo
// Commitment is set for commitment triggers.
Commitment *Commitment
// OutcomeObserved is set for world_changed: the desired state already holds.
// OutcomeObserved is set for world_changed when the desired state already
// holds, and for plan_completed when the plan's completion means the
// desired outcome was reached.
OutcomeObserved bool
}
@@ -805,6 +807,9 @@ func Reconsider(it Intention, t Trigger, rc ReconsiderContext) Reconsideration {
}
switch t.Kind {
case TriggerPlanCompleted:
if !t.OutcomeObserved {
return Reconsideration{Outcome: Abandon, Reason: "the plan finished without achieving the desired outcome (" + firstNonEmpty(t.Detail, "fallback strategy") + "); it has been handed to the operator"}
}
return Reconsideration{Outcome: Fulfil, Reason: "the plan completed and its checks passed"}
case TriggerGoalCompleted:
if it.Kind == IntentPursue || it.Kind == IntentInvestigate {
@@ -818,8 +823,8 @@ func Reconsider(it Intention, t Trigger, rc ReconsiderContext) Reconsideration {
case TriggerApprovalDenied:
return replanOr("the operator denied the request (" + firstNonEmpty(t.Detail, "no reason given") + ")")
case TriggerApprovalExpired:
if it.Status == IntentionBlocked && it.BlockedKind == BlockApproval && it.Reconsiderations > 0 {
return Reconsideration{Outcome: Suspend, Reason: "approval expired again without a decision; waiting rather than asking repeatedly"}
if rc.Replans > 0 {
return Reconsideration{Outcome: Suspend, Reason: "approval expired again without a decision; waiting rather than asking the operator repeatedly"}
}
return replanOr("the approval request expired before anyone decided")
case TriggerCapabilityChanged:
+4 -1
View File
@@ -278,7 +278,10 @@ func TestReconsider(t *testing.T) {
check(Trigger{Kind: TriggerCapabilityChanged, Action: restartAction("deny")}, ReconsiderContext{}, Replan)
check(Trigger{Kind: TriggerCapabilityChanged, Action: restartAction("ask")}, ReconsiderContext{}, Block)
check(Trigger{Kind: TriggerResourceExhausted, Detail: "capability invocations"}, ReconsiderContext{}, Suspend)
check(Trigger{Kind: TriggerPlanCompleted}, ReconsiderContext{}, Fulfil)
check(Trigger{Kind: TriggerApprovalExpired}, ReconsiderContext{}, Replan)
check(Trigger{Kind: TriggerApprovalExpired}, ReconsiderContext{Replans: 1}, Suspend)
check(Trigger{Kind: TriggerPlanCompleted, OutcomeObserved: true}, ReconsiderContext{}, Fulfil)
check(Trigger{Kind: TriggerPlanCompleted, Detail: "escalated"}, ReconsiderContext{}, Abandon)
check(Trigger{Kind: TriggerGoalCompleted}, ReconsiderContext{}, Abandon)
check(Trigger{Kind: TriggerWorldChanged, OutcomeObserved: true, Detail: "atlas already healthy"}, ReconsiderContext{}, Fulfil)
check(Trigger{Kind: TriggerCommitmentChanged, Commitment: &noProd, Action: restartAction("allow")}, ReconsiderContext{}, Tighten)