pi-agent: Implement operation coordination
This commit is contained in:
+78
-29
@@ -70,19 +70,26 @@ type Snapshot struct {
|
||||
|
||||
type TargetProgress struct {
|
||||
TargetID string
|
||||
// Applying is retained for synchronous DispatchProgress callers. New
|
||||
// interactive callers use the structured Phase field.
|
||||
Applying bool
|
||||
Phase TargetProgressPhase
|
||||
State target.State
|
||||
Evidence string
|
||||
Err error
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Snapshot Snapshot
|
||||
Notice string
|
||||
Confirmation string
|
||||
Progress []TargetProgress
|
||||
Complete bool
|
||||
Err error
|
||||
Snapshot Snapshot
|
||||
Notice string
|
||||
// Confirmation remains for existing synchronous callers.
|
||||
Confirmation string
|
||||
ConfirmationRequest *Confirmation
|
||||
Status OperationStatus
|
||||
Progress []TargetProgress
|
||||
FirstFailedTargetID string
|
||||
Complete bool
|
||||
Err error
|
||||
}
|
||||
|
||||
type Module interface {
|
||||
@@ -105,20 +112,22 @@ type ProgressiveDetectionModule interface {
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
store profile.Store
|
||||
state profile.State
|
||||
targets []target.Target
|
||||
targetIDs []string
|
||||
targetDescriptions []target.Description
|
||||
availabilities map[string]target.Availability
|
||||
inspections map[string]target.Inspection
|
||||
detectionPhase map[string]TargetDetectionPhase
|
||||
detectionErr map[string]error
|
||||
detectionGeneration uint64
|
||||
stateMu sync.RWMutex
|
||||
dispatchMu sync.Mutex
|
||||
mutationLock MutationLock
|
||||
logger OperationLogger
|
||||
store profile.Store
|
||||
state profile.State
|
||||
targets []target.Target
|
||||
targetIDs []string
|
||||
targetDescriptions []target.Description
|
||||
availabilities map[string]target.Availability
|
||||
inspections map[string]target.Inspection
|
||||
detectionPhase map[string]TargetDetectionPhase
|
||||
detectionErr map[string]error
|
||||
detectionGeneration uint64
|
||||
stateMu sync.RWMutex
|
||||
dispatchMu sync.Mutex
|
||||
interactiveMu sync.Mutex
|
||||
interactiveOperation bool
|
||||
mutationLock MutationLock
|
||||
logger OperationLogger
|
||||
}
|
||||
|
||||
func New(store profile.Store) (*Application, error) {
|
||||
@@ -290,7 +299,7 @@ func (a *Application) dispatch(intent Intent, publish func(Result)) Result {
|
||||
a.refreshInspections()
|
||||
}
|
||||
if intent.Kind == SetTargetDesired {
|
||||
return a.setTargetDesired(intent)
|
||||
return a.setTargetDesired(intent, publish)
|
||||
}
|
||||
if intent.Kind == RefreshTargets {
|
||||
a.refreshTargets()
|
||||
@@ -357,7 +366,13 @@ func (a *Application) activateProfile(intent Intent, publish func(Result)) Resul
|
||||
}
|
||||
}
|
||||
if len(warnings) > 0 && !intent.Confirm {
|
||||
return Result{Snapshot: a.Snapshot(), Confirmation: "Changing the Active Profile requires confirmation:\n" + strings.Join(warnings, "\n")}
|
||||
message := "Changing the Active Profile requires confirmation:\n" + strings.Join(warnings, "\n")
|
||||
confirmation := &Confirmation{
|
||||
Title: "Confirm Active Profile change", Summary: "Changing the Active Profile has high-impact Target effects.",
|
||||
Details: warnings, ConfirmLabel: "Change Profile", CancelLabel: "Cancel", HighImpact: true,
|
||||
}
|
||||
return Result{Snapshot: a.Snapshot(), Confirmation: message, ConfirmationRequest: confirmation,
|
||||
Status: OperationStatus{Kind: StatusConfirmation, Summary: confirmation.Title, Detail: confirmation.Summary}}
|
||||
}
|
||||
|
||||
next, err := a.store.Mutate(func(current profile.State) (profile.State, error) {
|
||||
@@ -395,7 +410,8 @@ func (a *Application) activateProfile(intent Intent, publish func(Result)) Resul
|
||||
name := a.targetDescriptions[targetIndex].Name
|
||||
updated[id] = true
|
||||
if publish != nil {
|
||||
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{{TargetID: id, Applying: true, State: a.inspection(id).State}}, Notice: "Updating " + name + " Target"})
|
||||
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{{TargetID: id, Applying: true, Phase: TargetProgressRunning, State: a.inspection(id).State}}, Notice: "Updating " + name + " Target",
|
||||
Status: OperationStatus{Kind: StatusProgress, Summary: "Updating " + name + " Target", TargetID: id}})
|
||||
}
|
||||
operation := adapter.SetDesired(context.Background(), true, active)
|
||||
inspection := operation.Inspection
|
||||
@@ -407,10 +423,18 @@ func (a *Application) activateProfile(intent Intent, publish func(Result)) Resul
|
||||
safeErr = fmt.Errorf("%s (fresh inspection: %s — %s)", profile.RedactText(operation.Err.Error()), inspection.State, profile.RedactText(inspection.Evidence))
|
||||
}
|
||||
a.replaceInspection(id, inspection)
|
||||
item := TargetProgress{TargetID: id, State: inspection.State, Evidence: inspection.Evidence, Err: safeErr}
|
||||
progressPhase := TargetProgressSucceeded
|
||||
if safeErr != nil {
|
||||
progressPhase = TargetProgressFailed
|
||||
}
|
||||
item := TargetProgress{TargetID: id, Phase: progressPhase, State: inspection.State, Evidence: inspection.Evidence, Err: safeErr}
|
||||
progress = append(progress, item)
|
||||
if publish != nil {
|
||||
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{item}, Notice: name + " Target update completed", Err: safeErr})
|
||||
status := OperationStatus{Kind: StatusSuccess, Summary: name + " Target update completed", TargetID: id}
|
||||
if safeErr != nil {
|
||||
status = OperationStatus{Kind: StatusError, Summary: name + " Target update failed", Detail: safeErr.Error(), TargetID: id}
|
||||
}
|
||||
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{item}, Notice: name + " Target update completed", Status: status, Err: safeErr})
|
||||
}
|
||||
if a.logger != nil {
|
||||
outcome, detail := "success", inspection.Evidence
|
||||
@@ -433,10 +457,19 @@ func (a *Application) activateProfile(intent Intent, publish func(Result)) Resul
|
||||
}
|
||||
}
|
||||
a.mergeInspections(remaining)
|
||||
result := Result{Snapshot: a.Snapshot(), Progress: progress, Notice: fmt.Sprintf("Activated Profile %q", intent.Name)}
|
||||
result := Result{Snapshot: a.Snapshot(), Progress: progress, Notice: fmt.Sprintf("Activated Profile %q", intent.Name),
|
||||
Status: OperationStatus{Kind: StatusSuccess, Summary: fmt.Sprintf("Activated Profile %q", intent.Name)}}
|
||||
if len(failures) > 0 {
|
||||
result.Err = errors.Join(failures...)
|
||||
result.Notice = fmt.Sprintf("Activated Profile %q with %d Target update failure(s); successful updates were kept and no global rollback was attempted", intent.Name, len(failures))
|
||||
result.Status = OperationStatus{Kind: StatusError, Summary: fmt.Sprintf("Activated Profile %q with %d Target update failure(s)", intent.Name, len(failures)), Detail: result.Err.Error()}
|
||||
for _, item := range progress {
|
||||
if item.Err != nil {
|
||||
result.FirstFailedTargetID = item.TargetID
|
||||
result.Status.TargetID = item.TargetID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -530,7 +563,7 @@ func (a *Application) refreshInspections() {
|
||||
a.replaceInspections(inspections)
|
||||
}
|
||||
|
||||
func (a *Application) setTargetDesired(intent Intent) Result {
|
||||
func (a *Application) setTargetDesired(intent Intent, publish func(Result)) Result {
|
||||
selectedIndex := -1
|
||||
for i, id := range a.targetIDs {
|
||||
if id == intent.TargetID {
|
||||
@@ -547,7 +580,14 @@ func (a *Application) setTargetDesired(intent Intent) Result {
|
||||
return Result{Snapshot: a.Snapshot(), Err: errors.New("no Active Profile; open Profile management before enabling a Target")}
|
||||
}
|
||||
if warning := selected.Confirmation(intent.Enabled); warning != "" && !intent.Confirm {
|
||||
return Result{Snapshot: a.Snapshot(), Confirmation: warning}
|
||||
confirmation := &Confirmation{Title: "Confirm " + a.targetDescriptions[selectedIndex].Name + " Target change", Summary: warning,
|
||||
ConfirmLabel: "Continue", CancelLabel: "Cancel", HighImpact: true}
|
||||
return Result{Snapshot: a.Snapshot(), Confirmation: warning, ConfirmationRequest: confirmation,
|
||||
Status: OperationStatus{Kind: StatusConfirmation, Summary: confirmation.Title, Detail: confirmation.Summary, TargetID: intent.TargetID}}
|
||||
}
|
||||
if publish != nil {
|
||||
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{{TargetID: intent.TargetID, Applying: true, Phase: TargetProgressRunning, State: a.inspection(intent.TargetID).State}},
|
||||
Status: OperationStatus{Kind: StatusProgress, Summary: "Updating " + a.targetDescriptions[selectedIndex].Name + " Target", TargetID: intent.TargetID}})
|
||||
}
|
||||
result := selected.SetDesired(context.Background(), intent.Enabled, active)
|
||||
inspection := result.Inspection
|
||||
@@ -567,10 +607,19 @@ func (a *Application) setTargetDesired(intent Intent) Result {
|
||||
_ = a.logger.Write(LogEntry{Time: time.Now(), Operation: operation, Target: intent.TargetID, Result: outcome, Detail: RedactLogDetail(detail)})
|
||||
}
|
||||
notice := ""
|
||||
progressPhase := TargetProgressSucceeded
|
||||
status := OperationStatus{Kind: StatusSuccess, TargetID: intent.TargetID}
|
||||
if result.Err == nil {
|
||||
notice = fmt.Sprintf("%s Target is now %s", a.targetDescriptions[selectedIndex].Name, inspection.State)
|
||||
status.Summary = notice
|
||||
} else {
|
||||
progressPhase = TargetProgressFailed
|
||||
status.Kind = StatusError
|
||||
status.Summary = a.targetDescriptions[selectedIndex].Name + " Target update failed"
|
||||
status.Detail = result.Err.Error()
|
||||
}
|
||||
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: result.Err}
|
||||
progress := TargetProgress{TargetID: intent.TargetID, Phase: progressPhase, State: inspection.State, Evidence: inspection.Evidence, Err: result.Err}
|
||||
return Result{Snapshot: a.Snapshot(), Notice: notice, Status: status, Progress: []TargetProgress{progress}, FirstFailedTargetID: map[bool]string{true: intent.TargetID}[result.Err != nil], Err: result.Err}
|
||||
}
|
||||
|
||||
func create(state profile.State, input profile.Profile) (profile.State, string, error) {
|
||||
|
||||
Reference in New Issue
Block a user