mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat: agent self evolution (#2847)
* feat: add agent self-evolution * fix ci * delete unused doc * fix lint * fix evolution review issues
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package evolution
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
)
|
||||
|
||||
type LifecycleRunSummary struct {
|
||||
EvaluatedProfiles int
|
||||
TransitionedProfiles int
|
||||
DeletedSkills int
|
||||
}
|
||||
|
||||
func NextLifecycleState(profile SkillProfile, now time.Time) SkillStatus {
|
||||
if profile.Origin == "manual" || profile.LastUsedAt.IsZero() {
|
||||
return profile.Status
|
||||
}
|
||||
|
||||
idle := now.Sub(profile.LastUsedAt)
|
||||
switch profile.Status {
|
||||
case SkillStatusActive:
|
||||
if idle > 90*24*time.Hour && profile.RetentionScore < 0.3 {
|
||||
return SkillStatusCold
|
||||
}
|
||||
case SkillStatusCold:
|
||||
if idle > 180*24*time.Hour && profile.RetentionScore < 0.2 {
|
||||
return SkillStatusArchived
|
||||
}
|
||||
case SkillStatusArchived:
|
||||
if idle > 365*24*time.Hour && profile.RetentionScore < 0.1 {
|
||||
return SkillStatusDeleted
|
||||
}
|
||||
}
|
||||
|
||||
return profile.Status
|
||||
}
|
||||
|
||||
func ApplyLifecycleState(paths Paths, profile SkillProfile, next SkillStatus) error {
|
||||
if next != SkillStatusDeleted {
|
||||
return nil
|
||||
}
|
||||
|
||||
workspace := profile.WorkspaceID
|
||||
if workspace == "" {
|
||||
workspace = inferWorkspaceFromPaths(paths)
|
||||
}
|
||||
if workspace == "" {
|
||||
return fmt.Errorf("resolve lifecycle delete workspace for skill %q: workspace is required", profile.SkillName)
|
||||
}
|
||||
if err := skills.ValidateSkillName(profile.SkillName); err != nil {
|
||||
return fmt.Errorf("resolve lifecycle delete skill name: %w", err)
|
||||
}
|
||||
|
||||
skillPath := filepath.Join(workspace, "skills", profile.SkillName, "SKILL.md")
|
||||
err := os.Remove(skillPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func RunLifecycleOnce(store *Store, paths Paths, workspace string, now time.Time) (LifecycleRunSummary, error) {
|
||||
if store == nil {
|
||||
return LifecycleRunSummary{}, nil
|
||||
}
|
||||
|
||||
profiles, err := store.LoadProfiles()
|
||||
if err != nil {
|
||||
return LifecycleRunSummary{}, err
|
||||
}
|
||||
|
||||
summary := LifecycleRunSummary{}
|
||||
for _, profile := range profiles {
|
||||
if !profileBelongsToWorkspace(paths, workspace, profile) {
|
||||
continue
|
||||
}
|
||||
|
||||
summary.EvaluatedProfiles++
|
||||
next := NextLifecycleState(profile, now)
|
||||
if next == profile.Status {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ApplyLifecycleState(paths, profile, next); err != nil {
|
||||
return summary, err
|
||||
}
|
||||
profile.VersionHistory = append(profile.VersionHistory, SkillVersionEntry{
|
||||
Version: profile.CurrentVersion,
|
||||
Action: "lifecycle:" + string(next),
|
||||
Timestamp: now,
|
||||
Summary: fmt.Sprintf("lifecycle transition: %s -> %s", profile.Status, next),
|
||||
})
|
||||
profile.Status = next
|
||||
if err := store.SaveProfile(profile); err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
summary.TransitionedProfiles++
|
||||
if next == SkillStatusDeleted {
|
||||
summary.DeletedSkills++
|
||||
}
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func inferWorkspaceFromPaths(paths Paths) string {
|
||||
root := filepath.Clean(paths.RootDir)
|
||||
if filepath.Base(root) != "evolution" {
|
||||
return ""
|
||||
}
|
||||
stateDir := filepath.Dir(root)
|
||||
if filepath.Base(stateDir) != "state" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Dir(stateDir)
|
||||
}
|
||||
|
||||
func profileBelongsToWorkspace(paths Paths, workspace string, profile SkillProfile) bool {
|
||||
if profile.WorkspaceID == workspace {
|
||||
return true
|
||||
}
|
||||
return profile.WorkspaceID == "" && usesDefaultWorkspaceState(paths, workspace)
|
||||
}
|
||||
|
||||
func usesDefaultWorkspaceState(paths Paths, workspace string) bool {
|
||||
return paths.RootDir == NewPaths(workspace, "").RootDir
|
||||
}
|
||||
Reference in New Issue
Block a user