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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user