picoclaw/pkg/evolution/runtime.go

1271 lines
36 KiB
Go

package evolution
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/skills"
)
var ErrApplyDraftFailed = errors.New("apply draft failed")
type RuntimeOptions struct {
Config config.EvolutionConfig
Now func() time.Time
Store *Store
Organizer *Organizer
SuccessJudge SuccessJudge
SkillsRecaller *SkillsRecaller
DraftGenerator DraftGenerator
GeneratorFactory func(workspace string) DraftGenerator
SuccessJudgeFactory func(workspace string) SuccessJudge
Applier *Applier
ApplierFactory func(workspace string) *Applier
}
type Runtime struct {
cfg config.EvolutionConfig
mu sync.Mutex
now func() time.Time
writer *CaseWriter
store *Store
organizer *Organizer
successJudge SuccessJudge
skillsRecaller *SkillsRecaller
draftGenerator DraftGenerator
generatorFactory func(workspace string) DraftGenerator
successJudgeFactory func(workspace string) SuccessJudge
applier *Applier
applierFactory func(workspace string) *Applier
}
type TurnCaseInput struct {
Workspace string
WorkspaceID string
TurnID string
SessionKey string
AgentID string
Status string
UserMessage string
FinalContent string
ToolKinds []string
ToolExecutions []ToolExecutionRecord
ActiveSkillNames []string
AttemptedSkillNames []string
FinalSuccessfulPath []string
SkillContextSnapshots []SkillContextSnapshot
}
func NewRuntime(opts RuntimeOptions) (*Runtime, error) {
now := opts.Now
if now == nil {
now = time.Now
}
organizer := opts.Organizer
if organizer == nil {
organizer = NewOrganizer(OrganizerOptions{
MinCaseCount: opts.Config.MinCaseCount,
MinSuccessRate: opts.Config.MinSuccessRate,
Now: now,
})
}
return &Runtime{
cfg: opts.Config,
now: now,
store: opts.Store,
organizer: organizer,
successJudge: opts.SuccessJudge,
skillsRecaller: opts.SkillsRecaller,
draftGenerator: opts.DraftGenerator,
generatorFactory: opts.GeneratorFactory,
successJudgeFactory: opts.SuccessJudgeFactory,
applier: opts.Applier,
applierFactory: opts.ApplierFactory,
}, nil
}
func (rt *Runtime) FinalizeTurn(ctx context.Context, input TurnCaseInput) error {
if rt == nil || !rt.cfg.Enabled || input.Workspace == "" || shouldSkipLearningRecord(input) {
return nil
}
success := input.Status == "completed"
skillUsage := buildSkillUsage(input)
workspaceID := input.WorkspaceID
if workspaceID == "" {
workspaceID = input.Workspace
}
toolKinds := cloneToolKinds(input)
record := LearningRecord{
ID: input.TurnID,
Kind: RecordKindTask,
WorkspaceID: workspaceID,
CreatedAt: rt.now(),
SessionKey: input.SessionKey,
TaskHash: buildTaskHash(input, skillUsage, toolKinds),
Summary: buildRecordSummary(input),
UserGoal: summarizeText(input.UserMessage, 240),
FinalOutput: summarizeText(input.FinalContent, 240),
Source: map[string]any{"turn_id": input.TurnID, "session_key": input.SessionKey, "agent_id": input.AgentID},
Status: RecordStatus("new"),
Success: &success,
ToolKinds: toolKinds,
ToolExecutions: cloneToolExecutions(input.ToolExecutions),
InitialSkillNames: append([]string(nil), skillUsage.Initial...),
AddedSkillNames: append([]string(nil), skillUsage.Added...),
UsedSkillNames: append([]string(nil), skillUsage.Used...),
AllLoadedSkillNames: append([]string(nil), skillUsage.All...),
ActiveSkillNames: append([]string(nil), skillUsage.Initial...),
AttemptTrail: buildAttemptTrail(skillUsage, success),
Signals: buildLearningSignals(skillUsage, success),
}
paths := NewPaths(input.Workspace, rt.cfg.StateDir)
rt.mu.Lock()
if rt.writer == nil || rt.writer.paths.RootDir != paths.RootDir {
rt.writer = NewCaseWriter(paths)
}
writer := rt.writer
rt.mu.Unlock()
if err := writer.AppendCase(ctx, record); err != nil {
return err
}
if err := rt.recordSkillUsage(input, success); err != nil {
return err
}
logger.InfoCF("evolution", "Recorded hot path learning record", map[string]any{
"workspace": input.Workspace,
"turn_id": input.TurnID,
"success": success,
"tool_count": len(record.ToolExecutions),
"used_skills": len(record.UsedSkillNames),
"all_skills": len(record.AllLoadedSkillNames),
})
return nil
}
type skillUsageRecord struct {
Initial []string
Added []string
Used []string
All []string
Attempted []string
Final []string
}
func buildAttemptTrail(usage skillUsageRecord, success bool) *AttemptTrail {
if len(usage.All) == 0 {
return nil
}
trail := &AttemptTrail{
AttemptedSkills: append([]string(nil), usage.Attempted...),
}
if success {
finalPath := usage.Final
if len(finalPath) == 0 {
finalPath = usage.All
}
trail.FinalSuccessfulPath = append([]string(nil), finalPath...)
}
return trail
}
func buildTaskHash(input TurnCaseInput, usage skillUsageRecord, toolKinds []string) string {
parts := make([]string, 0, len(usage.All)+len(toolKinds)+3)
parts = append(parts, normalizedValues(usage.All)...)
parts = append(parts, normalizedValues(toolKinds)...)
if goal := summarizeText(input.UserMessage, 120); goal != "" {
parts = append(parts, strings.ToLower(goal))
}
if strings.TrimSpace(input.Status) != "" {
parts = append(parts, strings.ToLower(strings.TrimSpace(input.Status)))
}
if len(parts) == 0 {
parts = append(parts, "empty")
}
sum := sha1.Sum([]byte(strings.Join(parts, "\x00")))
return hex.EncodeToString(sum[:8])
}
func buildLearningSignals(usage skillUsageRecord, success bool) []string {
if !success || len(usage.Used) == 0 {
return nil
}
return []string{"potentially_learnable"}
}
func normalizedValues(values []string) []string {
out := make([]string, 0, len(values))
for _, value := range values {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
continue
}
out = append(out, value)
}
return out
}
func buildRecordSummary(input TurnCaseInput) string {
if goal := summarizeText(input.UserMessage, 160); goal != "" {
return goal
}
return fmt.Sprintf("turn %s finished with status=%s", input.TurnID, input.Status)
}
func summarizeText(text string, maxLen int) string {
text = strings.TrimSpace(text)
if text == "" || maxLen <= 0 {
return text
}
if utf8.RuneCountInString(text) <= maxLen {
return text
}
if maxLen <= 3 {
runes := []rune(text)
return string(runes[:maxLen])
}
runes := []rune(text)
return string(runes[:maxLen-3]) + "..."
}
func cloneToolKinds(input TurnCaseInput) []string {
if len(input.ToolKinds) > 0 {
return append([]string(nil), input.ToolKinds...)
}
if len(input.ToolExecutions) == 0 {
return nil
}
out := make([]string, 0, len(input.ToolExecutions))
seen := make(map[string]struct{}, len(input.ToolExecutions))
for _, exec := range input.ToolExecutions {
name := strings.TrimSpace(exec.Name)
if name == "" {
continue
}
if _, ok := seen[name]; ok {
continue
}
seen[name] = struct{}{}
out = append(out, name)
}
return out
}
func cloneToolExecutions(input []ToolExecutionRecord) []ToolExecutionRecord {
if len(input) == 0 {
return nil
}
out := make([]ToolExecutionRecord, 0, len(input))
for _, exec := range input {
name := strings.TrimSpace(exec.Name)
if name == "" {
continue
}
out = append(out, ToolExecutionRecord{
Name: name,
Success: exec.Success,
ErrorSummary: strings.TrimSpace(exec.ErrorSummary),
SkillNames: uniqueTrimmedNames(exec.SkillNames),
})
}
return out
}
func buildSkillUsage(input TurnCaseInput) skillUsageRecord {
initial := uniqueTrimmedNames(input.ActiveSkillNames)
if len(input.SkillContextSnapshots) > 0 {
initial = uniqueTrimmedNames(input.SkillContextSnapshots[0].SkillNames)
}
all := append([]string(nil), initial...)
added := make([]string, 0)
seen := make(map[string]struct{}, len(all))
for _, skillName := range all {
seen[strings.ToLower(skillName)] = struct{}{}
}
appendNew := func(skillNames []string) {
for _, skillName := range uniqueTrimmedNames(skillNames) {
key := strings.ToLower(skillName)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
added = append(added, skillName)
all = append(all, skillName)
}
}
for _, snapshot := range input.SkillContextSnapshots {
appendNew(snapshot.SkillNames)
}
for _, exec := range input.ToolExecutions {
appendNew(exec.SkillNames)
}
attempted := uniqueTrimmedNames(input.AttemptedSkillNames)
if len(attempted) == 0 {
attempted = append([]string(nil), all...)
} else {
appendNew(attempted)
}
final := uniqueTrimmedNames(input.FinalSuccessfulPath)
if len(final) > 0 {
appendNew(final)
}
if len(attempted) == 0 {
attempted = append([]string(nil), all...)
}
if len(final) == 0 {
final = append([]string(nil), added...)
}
return skillUsageRecord{
Initial: initial,
Added: append([]string(nil), added...),
Used: append([]string(nil), added...),
All: all,
Attempted: append([]string(nil), attempted...),
Final: append([]string(nil), final...),
}
}
func shouldSkipLearningRecord(input TurnCaseInput) bool {
if strings.EqualFold(strings.TrimSpace(input.SessionKey), "heartbeat") {
return true
}
return false
}
func uniqueTrimmedNames(values []string) []string {
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, value)
}
return out
}
func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error {
if rt == nil || !rt.cfg.Enabled || workspace == "" {
return nil
}
mode := rt.cfg.EffectiveMode()
runID := fmt.Sprintf("%d", rt.now().UnixNano())
if mode == "" || mode == "observe" {
logger.InfoCF("evolution", "Skipped cold path run", map[string]any{
"workspace": workspace,
"mode": mode,
"run_id": runID,
})
return nil
}
logger.InfoCF("evolution", "Started cold path run", map[string]any{
"workspace": workspace,
"mode": mode,
"run_id": runID,
})
store := rt.storeForWorkspace(workspace)
records, err := store.LoadLearningRecords()
if err != nil {
return err
}
logger.InfoCF("evolution", "Loaded evolution records", map[string]any{
"workspace": workspace,
"record_count": len(records),
"run_id": runID,
})
admittedCount := 0
newRuleCount := 0
if rt.organizer != nil {
recordsForOrganizer, err := rt.recordsForColdPath(ctx, workspace, records)
if err != nil {
return err
}
admittedCount = countTaskLearningRecords(recordsForOrganizer)
logger.InfoCF("evolution", "Admitted task records for cold path", map[string]any{
"workspace": workspace,
"admitted_tasks": admittedCount,
"organizer_input": len(recordsForOrganizer),
"task_ids": joinRecordIDs(recordsForOrganizer),
"run_id": runID,
})
rules, err := rt.organizer.BuildRules(recordsForOrganizer)
if err != nil {
return err
}
newRules := filterNewRules(records, rules, workspace)
newRuleCount = len(newRules)
logger.InfoCF("evolution", "Built learning patterns", map[string]any{
"workspace": workspace,
"pattern_count": len(rules),
"new_patterns": len(newRules),
"admitted_tasks": admittedCount,
"patterns": summarizePatternRecords(rules),
"run_id": runID,
})
if len(newRules) > 0 {
if err := store.AppendLearningRecords(newRules); err != nil {
return err
}
records = append(records, newRules...)
}
}
generator := rt.draftGeneratorForWorkspace(workspace)
if generator == nil {
logger.InfoCF("evolution", "Skipped drafting because no draft generator is available", map[string]any{
"workspace": workspace,
"run_id": runID,
})
return rt.runLifecycleMaintenance(workspace, store, runID)
}
recaller := rt.skillsRecallerForWorkspace(workspace)
applier := rt.applierForWorkspace(workspace)
readyRules := filterReadyRules(records, workspace)
if len(readyRules) == 0 {
logger.InfoCF("evolution", "Finished cold path run without ready patterns", map[string]any{
"workspace": workspace,
"record_count": len(records),
"new_patterns": newRuleCount,
"admitted_tasks": admittedCount,
"run_id": runID,
})
return rt.runLifecycleMaintenance(workspace, store, runID)
}
existingDrafts, err := store.LoadDrafts()
if err != nil {
return err
}
existingBySource := existingDraftSourceSet(existingDrafts, workspace)
logger.InfoCF("evolution", "Selected ready patterns for drafting", map[string]any{
"workspace": workspace,
"ready_patterns": len(readyRules),
"existing_draft_count": len(existingBySource),
"ready_pattern_ids": joinRecordIDs(readyRules),
"ready_patterns_info": summarizePatternRecords(readyRules),
"run_id": runID,
})
processedRules := 0
for _, rule := range readyRules {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if _, exists := existingBySource[rule.ID]; exists {
logger.InfoCF("evolution", "Skipped pattern because a non-quarantined draft already exists", map[string]any{
"workspace": workspace,
"pattern_id": rule.ID,
"pattern_info": summarizePatternRecord(rule),
"run_id": runID,
})
continue
}
matches, err := recaller.RecallSimilarSkills(rule)
if err != nil {
return err
}
logger.InfoCF("evolution", "Generating skill draft", map[string]any{
"workspace": workspace,
"pattern_id": rule.ID,
"matched_skill_count": len(matches),
"pattern_info": summarizePatternRecord(rule),
"run_id": runID,
})
draft, err := generator.GenerateDraft(ctx, rule, matches)
if err != nil {
return err
}
draft = rt.finalizeDraft(workspace, rule, matches, draft)
logger.InfoCF("evolution", "Finalized skill draft", map[string]any{
"workspace": workspace,
"pattern_id": rule.ID,
"draft_id": draft.ID,
"target_skill": draft.TargetSkillName,
"change_kind": string(draft.ChangeKind),
"status": string(draft.Status),
"run_id": runID,
})
if mode == "apply" && applier != nil && draft.Status == DraftStatusCandidate {
logger.InfoCF("evolution", "Applying skill draft", map[string]any{
"workspace": workspace,
"draft_id": draft.ID,
"target_skill": draft.TargetSkillName,
"change_kind": string(draft.ChangeKind),
"run_id": runID,
})
rollbackApply, err := applier.applyDraftWithRollback(ctx, workspace, draft)
if err != nil {
logger.WarnCF("evolution", "Skill draft apply failed", map[string]any{
"workspace": workspace,
"draft_id": draft.ID,
"target_skill": draft.TargetSkillName,
"error": err.Error(),
"run_id": runID,
})
draft.Status = DraftStatusQuarantined
draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("apply failed: %v", err))
if auditErr := rt.recordRollbackAudit(store, draft, err); auditErr != nil {
draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("rollback audit failed: %v", auditErr))
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), auditErr, saveErr)
}
return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), auditErr)
}
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), saveErr)
}
return fmt.Errorf("%w: %v", ErrApplyDraftFailed, err)
}
draft.Status = DraftStatusAccepted
if err := rt.saveAppliedProfile(store, workspace, draft); err != nil {
logger.WarnCF("evolution", "Skill profile save failed after apply", map[string]any{
"workspace": workspace,
"draft_id": draft.ID,
"target_skill": draft.TargetSkillName,
"error": err.Error(),
"run_id": runID,
})
draft.Status = DraftStatusQuarantined
draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("profile save failed: %v", err))
if rollbackErr := rollbackApply(); rollbackErr != nil {
draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("apply rollback failed: %v", rollbackErr))
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), rollbackErr, saveErr)
}
return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), rollbackErr)
}
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), saveErr)
}
return fmt.Errorf("%w: %v", ErrApplyDraftFailed, err)
}
logger.InfoCF("evolution", "Applied skill draft successfully", map[string]any{
"workspace": workspace,
"draft_id": draft.ID,
"target_skill": draft.TargetSkillName,
"run_id": runID,
})
}
if err := store.SaveDrafts([]SkillDraft{draft}); err != nil {
return err
}
logger.InfoCF("evolution", "Saved skill draft", map[string]any{
"workspace": workspace,
"draft_id": draft.ID,
"target_skill": draft.TargetSkillName,
"status": string(draft.Status),
"run_id": runID,
})
existingBySource[rule.ID] = struct{}{}
processedRules++
}
logger.InfoCF("evolution", "Finished cold path run", map[string]any{
"workspace": workspace,
"ready_patterns": len(readyRules),
"processed_patterns": processedRules,
"new_patterns": newRuleCount,
"run_id": runID,
})
return rt.runLifecycleMaintenance(workspace, store, runID)
}
func (rt *Runtime) recordsForColdPath(
ctx context.Context,
workspace string,
records []LearningRecord,
) ([]LearningRecord, error) {
out := make([]LearningRecord, 0, len(records))
judge := rt.successJudgeForWorkspace(workspace)
for _, record := range records {
if !isTaskRecordKind(record.Kind) {
out = append(out, record)
continue
}
if record.WorkspaceID != workspace {
continue
}
if !passesColdPathRuleFilter(record) {
continue
}
if judge != nil {
decision, err := judge.JudgeTaskRecord(ctx, record)
if err != nil {
return nil, err
}
if !decision.Success {
continue
}
}
out = append(out, record)
}
return out, nil
}
func passesColdPathRuleFilter(record LearningRecord) bool {
if !isTaskRecordKind(record.Kind) {
return false
}
if record.Success == nil || !*record.Success {
return false
}
if strings.TrimSpace(record.UserGoal) == "" {
return false
}
if strings.EqualFold(strings.TrimSpace(record.SessionKey), "heartbeat") {
return false
}
if strings.EqualFold(strings.TrimSpace(record.FinalOutput), "HEARTBEAT_OK") {
return false
}
if len(record.ToolExecutions) > 0 {
allFailed := true
for _, exec := range record.ToolExecutions {
if exec.Success {
allFailed = false
break
}
}
if allFailed {
return false
}
}
if len(record.ToolKinds) == 0 && len(record.UsedSkillNames) == 0 && strings.TrimSpace(record.FinalOutput) == "" {
return false
}
return true
}
func (rt *Runtime) storeForWorkspace(workspace string) *Store {
paths := NewPaths(workspace, rt.cfg.StateDir)
rt.mu.Lock()
defer rt.mu.Unlock()
if rt.store == nil || rt.store.paths.RootDir != paths.RootDir {
rt.store = NewStore(paths)
}
return rt.store
}
func (rt *Runtime) skillsRecallerForWorkspace(workspace string) *SkillsRecaller {
rt.mu.Lock()
defer rt.mu.Unlock()
if rt.skillsRecaller == nil || rt.skillsRecaller.workspace != workspace {
rt.skillsRecaller = NewSkillsRecaller(workspace)
}
return rt.skillsRecaller
}
func (rt *Runtime) draftGeneratorForWorkspace(workspace string) DraftGenerator {
if rt.generatorFactory != nil {
if generator := rt.generatorFactory(workspace); generator != nil {
return generator
}
}
if rt.draftGenerator != nil {
return rt.draftGenerator
}
return NewDefaultDraftGenerator(workspace)
}
func (rt *Runtime) successJudgeForWorkspace(workspace string) SuccessJudge {
if rt.successJudgeFactory != nil {
if judge := rt.successJudgeFactory(workspace); judge != nil {
return judge
}
}
if rt.successJudge != nil {
return rt.successJudge
}
return &HeuristicSuccessJudge{}
}
func (rt *Runtime) applierForWorkspace(workspace string) *Applier {
if rt.applierFactory != nil {
if applier := rt.applierFactory(workspace); applier != nil {
return applier
}
}
return rt.applier
}
func (rt *Runtime) finalizeDraft(workspace string, rule LearningRecord, matches []skills.SkillInfo, draft SkillDraft) SkillDraft {
if draft.ID == "" {
draft.ID = "draft-" + rule.ID
}
if draft.CreatedAt.IsZero() {
draft.CreatedAt = rt.now()
}
draft.WorkspaceID = workspace
draft.SourceRecordID = rule.ID
if len(draft.MatchedSkillRefs) == 0 {
draft.MatchedSkillRefs = collectSkillRefs(matches)
}
draft, normalizationNotes := rt.normalizeDraftForWorkspace(workspace, rule, draft)
review := ReviewDraft(draft)
draft.Status = review.Status
draft.ReviewNotes = append([]string(nil), review.ReviewNotes...)
draft.ReviewNotes = append(draft.ReviewNotes, normalizationNotes...)
if len(review.Findings) == 0 {
draft.ScanFindings = nil
return draft
}
draft.ScanFindings = append([]string(nil), review.Findings...)
return draft
}
func (rt *Runtime) normalizeDraftForWorkspace(
workspace string,
rule LearningRecord,
draft SkillDraft,
) (SkillDraft, []string) {
target := strings.TrimSpace(draft.TargetSkillName)
if workspace == "" || target == "" {
return draft, nil
}
notes := make([]string, 0, 4)
if combinedTarget := inferCombinedSkillName(rule); combinedTarget != "" && combinedTarget != target {
originalTarget := target
draft.TargetSkillName = combinedTarget
target = combinedTarget
notes = append(notes, fmt.Sprintf(
"retargeted draft from %q to combined shortcut skill %q because the winning path was a stable multi-skill chain",
originalTarget,
combinedTarget,
))
}
skillPath := filepath.Join(workspace, "skills", target, "SKILL.md")
_, err := os.Stat(skillPath)
hasExisting := err == nil
if err != nil && !errors.Is(err, os.ErrNotExist) {
return draft, notes
}
if combinedTarget := inferCombinedSkillName(rule); combinedTarget != "" && combinedTarget == target {
draft.HumanSummary = buildCombinedSkillHumanSummary(target, rule, hasExisting)
draft.PreferredEntryPath = []string{target}
draft.AvoidPatterns = appendUniqueStrings(
draft.AvoidPatterns,
buildCombinedSkillAvoidPattern(target, rule),
)
if hasExisting {
draft.ChangeKind = ChangeKindAppend
draft.BodyOrPatch = synthesizeCombinedSkillAppendBody(target, draft, rule)
notes = append(notes, "normalized combined shortcut draft to append onto the existing combined skill")
} else {
draft.ChangeKind = ChangeKindCreate
draft.BodyOrPatch = synthesizeCombinedSkillDocument(target, draft, rule)
notes = append(notes, "normalized combined shortcut draft to create a new standalone shortcut skill")
}
return draft, notes
}
if !hasExisting {
switch draft.ChangeKind {
case ChangeKindAppend, ChangeKindMerge, ChangeKindReplace:
draft.ChangeKind = ChangeKindCreate
notes = append(notes, "normalized change_kind to create because target skill did not exist")
if !looksLikeSkillDocument(draft.BodyOrPatch) {
draft.BodyOrPatch = synthesizeSkillDocumentFromPartialDraft(target, draft, rule)
notes = append(notes, "synthesized full skill document because draft body was partial")
}
}
return draft, notes
}
if draft.ChangeKind == ChangeKindCreate && !looksLikeSkillDocument(draft.BodyOrPatch) {
draft.ChangeKind = ChangeKindAppend
notes = append(notes, "normalized change_kind to append because target skill already existed")
}
return draft, notes
}
func looksLikeSkillDocument(body string) bool {
body = strings.TrimSpace(body)
return strings.HasPrefix(body, "---\n") && strings.Contains(body, "\n# ")
}
func synthesizeSkillDocumentFromPartialDraft(target string, draft SkillDraft, rule LearningRecord) string {
description := strings.TrimSpace(draft.HumanSummary)
if description == "" {
description = fmt.Sprintf("Learned workflow for %s.", target)
}
bodyContent := strings.TrimSpace(draft.BodyOrPatch)
if bodyContent == "" {
bodyContent = "No learned content was generated."
}
if strings.HasPrefix(bodyContent, "# ") {
return buildSkillDocument(target, description, bodyContent)
}
body := strings.Join([]string{
"# " + titleCaseSkillName(target),
"",
"## Start Here",
synthesizedStartHereLine(rule, target),
"",
"## Learned Evolution",
bodyContent,
"",
}, "\n")
return buildSkillDocument(target, description, body)
}
func synthesizeCombinedSkillDocument(target string, draft SkillDraft, rule LearningRecord) string {
description := strings.TrimSpace(draft.HumanSummary)
if description == "" {
description = buildCombinedSkillHumanSummary(target, rule, false)
}
body := strings.Join([]string{
"# " + titleCaseSkillName(target),
"",
"## Start Here",
synthesizedCombinedStartHereLine(rule, target),
"",
"## When To Use",
synthesizedCombinedWhenToUseLine(rule, target),
"",
"## Wrapped Path",
synthesizedWrappedPathLine(rule),
"",
"## Learned Shortcut",
synthesizedCombinedLearnedContent(draft.BodyOrPatch),
"",
}, "\n")
return buildSkillDocument(target, description, body)
}
func synthesizeCombinedSkillAppendBody(target string, draft SkillDraft, rule LearningRecord) string {
lines := []string{
"## Learned Shortcut Update",
fmt.Sprintf("- Shortcut skill: `%s`", target),
fmt.Sprintf("- Task summary: %s", fallbackEvolutionSummary(rule)),
fmt.Sprintf("- Wrapped path: %s", synthesizedWrappedPathLine(rule)),
"- Guidance: prefer this shortcut directly instead of replaying the whole path when the task matches.",
"",
synthesizedCombinedLearnedContent(draft.BodyOrPatch),
"",
}
return strings.Join(lines, "\n")
}
func synthesizedStartHereLine(rule LearningRecord, target string) string {
if len(rule.WinningPath) > 0 {
return fmt.Sprintf("Start with `%s` for tasks like `%s`.", strings.Join(rule.WinningPath, " -> "), strings.TrimSpace(rule.Summary))
}
if summary := strings.TrimSpace(rule.Summary); summary != "" {
return fmt.Sprintf("Use `%s` when the task matches `%s`.", target, summary)
}
return fmt.Sprintf("Use `%s` for the learned task pattern.", target)
}
func synthesizedCombinedStartHereLine(rule LearningRecord, target string) string {
return fmt.Sprintf("Use `%s` directly when the task matches `%s`.", target, fallbackEvolutionSummary(rule))
}
func synthesizedCombinedWhenToUseLine(rule LearningRecord, target string) string {
if len(rule.WinningPath) == 0 {
return fmt.Sprintf("Use `%s` when the learned task pattern appears again.", target)
}
return fmt.Sprintf("Use `%s` as a direct shortcut instead of replaying `%s` step by step.", target, strings.Join(rule.WinningPath, " -> "))
}
func synthesizedWrappedPathLine(rule LearningRecord) string {
if len(rule.WinningPath) == 0 {
return "No explicit wrapped path was recorded."
}
return strings.Join(rule.WinningPath, " -> ")
}
func synthesizedCombinedLearnedContent(body string) string {
content := strings.TrimSpace(stripSkillFrontmatter(body))
if content == "" {
return "Use this shortcut directly when the same task pattern appears again."
}
return demoteMarkdownHeadings(content)
}
func stripSkillFrontmatter(body string) string {
trimmed := strings.TrimSpace(body)
if !strings.HasPrefix(trimmed, "---\n") {
return trimmed
}
rest := strings.TrimPrefix(trimmed, "---\n")
end := strings.Index(rest, "\n---\n")
if end < 0 {
return trimmed
}
return strings.TrimSpace(rest[end+5:])
}
func demoteMarkdownHeadings(content string) string {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimLeft(line, " \t")
if !strings.HasPrefix(trimmed, "#") {
continue
}
prefixLen := len(line) - len(trimmed)
lines[i] = line[:prefixLen] + "##" + trimmed
}
return strings.TrimSpace(strings.Join(lines, "\n"))
}
func fallbackEvolutionSummary(rule LearningRecord) string {
if summary := strings.TrimSpace(rule.Summary); summary != "" {
return summary
}
if len(rule.WinningPath) > 0 {
return strings.Join(rule.WinningPath, " -> ")
}
return "the learned task pattern"
}
func buildCombinedSkillHumanSummary(target string, rule LearningRecord, hasExisting bool) string {
action := "Create"
if hasExisting {
action = "Refresh"
}
return fmt.Sprintf("%s combined shortcut %s from learned pattern: %s", action, target, fallbackEvolutionSummary(rule))
}
func buildCombinedSkillAvoidPattern(target string, rule LearningRecord) string {
if len(rule.WinningPath) == 0 {
return fmt.Sprintf("avoid bypassing `%s` when the same learned task pattern appears again", target)
}
return fmt.Sprintf("avoid replaying %s before trying `%s` directly", strings.Join(rule.WinningPath, " -> "), target)
}
func collectSkillRefs(matches []skills.SkillInfo) []string {
if len(matches) == 0 {
return nil
}
refs := make([]string, 0, len(matches))
for _, match := range matches {
if strings := match.Path; strings != "" {
refs = append(refs, strings)
continue
}
refs = append(refs, match.Source+":"+match.Name)
}
return refs
}
func countTaskLearningRecords(records []LearningRecord) int {
count := 0
for _, record := range records {
if isTaskRecordKind(record.Kind) {
count++
}
}
return count
}
func (rt *Runtime) runLifecycleMaintenance(workspace string, store *Store, runID string) error {
if rt == nil || store == nil || workspace == "" {
return nil
}
paths := NewPaths(workspace, rt.cfg.StateDir)
logger.InfoCF("evolution", "Started lifecycle maintenance", map[string]any{
"workspace": workspace,
"run_id": runID,
})
summary, err := RunLifecycleOnce(store, paths, workspace, rt.now())
if err != nil {
logger.WarnCF("evolution", "Lifecycle maintenance failed", map[string]any{
"workspace": workspace,
"run_id": runID,
"error": err.Error(),
})
return err
}
logger.InfoCF("evolution", "Finished lifecycle maintenance", map[string]any{
"workspace": workspace,
"run_id": runID,
"evaluated_profiles": summary.EvaluatedProfiles,
"transitioned_profiles": summary.TransitionedProfiles,
"deleted_skills": summary.DeletedSkills,
})
return nil
}
func joinRecordIDs(records []LearningRecord) string {
if len(records) == 0 {
return ""
}
ids := make([]string, 0, len(records))
for _, record := range records {
if strings.TrimSpace(record.ID) == "" {
continue
}
ids = append(ids, record.ID)
}
return strings.Join(ids, ",")
}
func summarizePatternRecords(records []LearningRecord) string {
if len(records) == 0 {
return ""
}
parts := make([]string, 0, len(records))
for _, record := range records {
parts = append(parts, summarizePatternRecord(record))
}
return strings.Join(parts, " | ")
}
func summarizePatternRecord(record LearningRecord) string {
label := strings.TrimSpace(record.ID)
if label == "" {
label = "unknown-pattern"
}
path := strings.Join(record.WinningPath, " -> ")
if path == "" {
path = strings.TrimSpace(record.Summary)
}
if path == "" {
path = "no-summary"
}
return fmt.Sprintf("%s[%s]", label, path)
}
func filterNewRules(records []LearningRecord, rules []LearningRecord, workspace string) []LearningRecord {
existing := make(map[string]struct{}, len(records))
for _, record := range records {
if !isPatternRecordKind(record.Kind) || record.WorkspaceID != workspace {
continue
}
existing[record.ID] = struct{}{}
}
out := make([]LearningRecord, 0, len(rules))
for _, rule := range rules {
if rule.WorkspaceID != workspace {
continue
}
if _, ok := existing[rule.ID]; ok {
continue
}
existing[rule.ID] = struct{}{}
out = append(out, rule)
}
return out
}
func filterReadyRules(records []LearningRecord, workspace string) []LearningRecord {
seen := make(map[string]LearningRecord)
for _, record := range records {
if !isPatternRecordKind(record.Kind) || record.WorkspaceID != workspace || record.Status != RecordStatus("ready") {
continue
}
seen[record.ID] = record
}
out := make([]LearningRecord, 0, len(seen))
for _, record := range seen {
out = append(out, record)
}
sort.Slice(out, func(i, j int) bool {
if !out[i].CreatedAt.Equal(out[j].CreatedAt) {
return out[i].CreatedAt.Before(out[j].CreatedAt)
}
return out[i].ID < out[j].ID
})
return out
}
func existingDraftSourceSet(drafts []SkillDraft, workspace string) map[string]struct{} {
out := make(map[string]struct{}, len(drafts))
for _, draft := range drafts {
if draft.WorkspaceID != workspace || draft.SourceRecordID == "" {
continue
}
if draft.Status == DraftStatusQuarantined {
continue
}
out[draft.SourceRecordID] = struct{}{}
}
return out
}
func (rt *Runtime) saveAppliedProfile(store *Store, workspace string, draft SkillDraft) error {
now := rt.now()
return SaveAppliedProfile(store, workspace, draft, now)
}
func (rt *Runtime) recordRollbackAudit(store *Store, draft SkillDraft, applyErr error) error {
profile, err := store.LoadProfile(draft.TargetSkillName)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
now := rt.now()
profile.VersionHistory = append(profile.VersionHistory, SkillVersionEntry{
Version: profile.CurrentVersion,
Action: "rollback",
Timestamp: now,
DraftID: draft.ID,
Summary: fmt.Sprintf("Rolled back failed draft apply: %s", draft.HumanSummary),
Rollback: true,
RollbackReason: applyErr.Error(),
})
return store.SaveProfile(profile)
}
func profileOrigin(origin string) string {
if origin == "manual" {
return origin
}
return "evolved"
}
func appendUniqueStrings(existing []string, values ...string) []string {
seen := make(map[string]struct{}, len(existing))
for _, value := range existing {
seen[value] = struct{}{}
}
for _, value := range values {
if strings.TrimSpace(value) == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
existing = append(existing, value)
seen[value] = struct{}{}
}
return existing
}
func (rt *Runtime) recordSkillUsage(input TurnCaseInput, success bool) error {
usage := buildSkillUsage(input)
if len(usage.All) == 0 {
return nil
}
store := rt.storeForWorkspace(input.Workspace)
seen := make(map[string]struct{}, len(usage.All))
for _, skillName := range usage.All {
skillName = strings.TrimSpace(skillName)
if skillName == "" {
continue
}
if _, ok := seen[skillName]; ok {
continue
}
seen[skillName] = struct{}{}
if err := rt.touchSkillProfile(store, input, skillName, success); err != nil {
return err
}
}
return nil
}
func (rt *Runtime) touchSkillProfile(store *Store, input TurnCaseInput, skillName string, success bool) error {
now := rt.now()
profile, err := store.LoadProfile(skillName)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if errors.Is(err, os.ErrNotExist) {
profile = SkillProfile{
SkillName: skillName,
WorkspaceID: input.Workspace,
Status: SkillStatusActive,
Origin: "manual",
HumanSummary: skillName,
RetentionScore: 0.2,
}
}
profile.SkillName = skillName
profile.WorkspaceID = input.Workspace
if profile.Status == SkillStatusCold || profile.Status == SkillStatusArchived || profile.Status == "" {
profile.Status = SkillStatusActive
}
if profile.Origin == "" {
profile.Origin = "manual"
}
if strings.TrimSpace(profile.HumanSummary) == "" {
profile.HumanSummary = skillName
}
profile.LastUsedAt = now
profile.UseCount++
profile.RetentionScore = nextRetentionScore(profile.RetentionScore, success)
return store.SaveProfile(profile)
}
func nextRetentionScore(current float64, success bool) float64 {
increment := 0.05
if success {
increment = 0.1
}
current += increment
if current > 1 {
return 1
}
return current
}