fix ci
This commit is contained in:
parent
83de57c3b8
commit
07be3462df
25 changed files with 815 additions and 495 deletions
|
|
@ -34,7 +34,11 @@ type evolutionBridge struct {
|
|||
|
||||
const evolutionDirectDeliveryAttr = "evolution_direct_delivery"
|
||||
|
||||
func newEvolutionBridge(registry *AgentRegistry, cfg *config.Config, provider providers.LLMProvider) (*evolutionBridge, error) {
|
||||
func newEvolutionBridge(
|
||||
registry *AgentRegistry,
|
||||
cfg *config.Config,
|
||||
provider providers.LLMProvider,
|
||||
) (*evolutionBridge, error) {
|
||||
if cfg == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,7 +355,13 @@ func TestEvolutionBridge_ObserveTurnEndPayloadIncludesResolvedAttemptTrail(t *te
|
|||
sub := al.SubscribeEvents(16)
|
||||
defer al.UnsubscribeEvents(sub.ID)
|
||||
|
||||
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-observe-attempt-trail", "cli", "direct")
|
||||
resp, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-observe-attempt-trail",
|
||||
"cli",
|
||||
"direct",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -412,7 +418,13 @@ func TestEvolutionBridge_ObserveTurnEndUsesLatestSkillSnapshotAfterRetry(t *test
|
|||
sub := al.SubscribeEvents(16)
|
||||
defer al.UnsubscribeEvents(sub.ID)
|
||||
|
||||
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-observe-retry-snapshot", "cli", "direct")
|
||||
resp, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-observe-retry-snapshot",
|
||||
"cli",
|
||||
"direct",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -437,12 +449,21 @@ func TestEvolutionBridge_ObserveTurnEndUsesLatestSkillSnapshotAfterRetry(t *test
|
|||
t.Fatalf("len(SkillContextSnapshots) = %d, want 2", len(got))
|
||||
}
|
||||
if turnEndPayload.SkillContextSnapshots[0].Trigger != skillContextTriggerInitialBuild {
|
||||
t.Fatalf("SkillContextSnapshots[0].Trigger = %q, want %q", turnEndPayload.SkillContextSnapshots[0].Trigger, skillContextTriggerInitialBuild)
|
||||
t.Fatalf(
|
||||
"SkillContextSnapshots[0].Trigger = %q, want %q",
|
||||
turnEndPayload.SkillContextSnapshots[0].Trigger,
|
||||
skillContextTriggerInitialBuild,
|
||||
)
|
||||
}
|
||||
if turnEndPayload.SkillContextSnapshots[1].Trigger != skillContextTriggerContextRetryRebuild {
|
||||
t.Fatalf("SkillContextSnapshots[1].Trigger = %q, want %q", turnEndPayload.SkillContextSnapshots[1].Trigger, skillContextTriggerContextRetryRebuild)
|
||||
t.Fatalf(
|
||||
"SkillContextSnapshots[1].Trigger = %q, want %q",
|
||||
turnEndPayload.SkillContextSnapshots[1].Trigger,
|
||||
skillContextTriggerContextRetryRebuild,
|
||||
)
|
||||
}
|
||||
if got := turnEndPayload.SkillContextSnapshots[1].SkillNames; len(got) != 2 || got[0] != "base-skill" || got[1] != "late-skill" {
|
||||
if got := turnEndPayload.SkillContextSnapshots[1].SkillNames; len(got) != 2 || got[0] != "base-skill" ||
|
||||
got[1] != "late-skill" {
|
||||
t.Fatalf("SkillContextSnapshots[1].SkillNames = %v, want [base-skill late-skill]", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -500,7 +521,13 @@ func TestEvolutionBridge_ScheduledModeDoesNotRunColdPathAfterTurn(t *testing.T)
|
|||
}, &simpleMockProvider{response: "ok"})
|
||||
defer al.Close()
|
||||
|
||||
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-scheduled-cold-path", "cli", "direct")
|
||||
resp, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-scheduled-cold-path",
|
||||
"cli",
|
||||
"direct",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -525,7 +552,13 @@ func TestEvolutionBridge_DraftModeUsesProviderBackedDraftGenerator(t *testing.T)
|
|||
})
|
||||
defer al.Close()
|
||||
|
||||
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-auto-cold-path-llm", "cli", "direct")
|
||||
resp, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-auto-cold-path-llm",
|
||||
"cli",
|
||||
"direct",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -567,7 +600,13 @@ func TestEvolutionBridge_DraftModeUsesProviderDefaultModel(t *testing.T) {
|
|||
al := NewAgentLoop(cfg, bus.NewMessageBus(), provider)
|
||||
defer al.Close()
|
||||
|
||||
if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-auto-cold-path-model", "cli", "direct"); err != nil {
|
||||
if _, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-auto-cold-path-model",
|
||||
"cli",
|
||||
"direct",
|
||||
); err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -606,7 +645,13 @@ func TestEvolutionBridge_DraftModePrefersConfigDefaultModelName(t *testing.T) {
|
|||
al := NewAgentLoop(cfg, bus.NewMessageBus(), provider)
|
||||
defer al.Close()
|
||||
|
||||
if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-auto-cold-path-model-config", "cli", "direct"); err != nil {
|
||||
if _, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-auto-cold-path-model-config",
|
||||
"cli",
|
||||
"direct",
|
||||
); err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -629,7 +674,13 @@ func TestEvolutionBridge_DraftModeKeepsCandidateDraft(t *testing.T) {
|
|||
})
|
||||
defer al.Close()
|
||||
|
||||
if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-apply-no-auto-apply", "cli", "direct"); err != nil {
|
||||
if _, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-apply-no-auto-apply",
|
||||
"cli",
|
||||
"direct",
|
||||
); err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -665,7 +716,13 @@ func TestEvolutionBridge_ApplyModeAutomaticallyRunsColdPathAndAppliesMergeDraft(
|
|||
})
|
||||
defer al.Close()
|
||||
|
||||
if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-apply-merge", "cli", "direct"); err != nil {
|
||||
if _, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-apply-merge",
|
||||
"cli",
|
||||
"direct",
|
||||
); err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -705,7 +762,13 @@ func TestEvolutionBridge_ObserveModeDoesNotRunColdPathOrCreateDraftFile(t *testi
|
|||
}, &simpleMockProvider{response: "ok"})
|
||||
defer al.Close()
|
||||
|
||||
resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-no-auto-cold-path", "cli", "direct")
|
||||
resp, err := al.ProcessDirectWithChannel(
|
||||
context.Background(),
|
||||
"hello",
|
||||
"session-no-auto-cold-path",
|
||||
"cli",
|
||||
"direct",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessDirectWithChannel failed: %v", err)
|
||||
}
|
||||
|
|
@ -784,7 +847,11 @@ func TestEvolutionBridge_TurnEndUsesExplicitAttemptTrail(t *testing.T) {
|
|||
FinalSuccessfulPath: []string{"geocode", "weather"},
|
||||
SkillContextSnapshots: []SkillContextSnapshot{
|
||||
{Sequence: 1, Trigger: skillContextTriggerInitialBuild, SkillNames: []string{"weather"}},
|
||||
{Sequence: 2, Trigger: skillContextTriggerContextRetryRebuild, SkillNames: []string{"geocode", "weather"}},
|
||||
{
|
||||
Sequence: 2,
|
||||
Trigger: skillContextTriggerContextRetryRebuild,
|
||||
SkillNames: []string{"geocode", "weather"},
|
||||
},
|
||||
},
|
||||
ToolKinds: []string{"echo_text"},
|
||||
},
|
||||
|
|
@ -846,8 +913,8 @@ func TestEvolutionBridge_CloseRejectsLateTurnEndEvents(t *testing.T) {
|
|||
t.Fatalf("newEvolutionBridge: %v", err)
|
||||
}
|
||||
|
||||
if err := bridge.Close(); err != nil {
|
||||
t.Fatalf("Close() error = %v", err)
|
||||
if closeErr := bridge.Close(); closeErr != nil {
|
||||
t.Fatalf("Close() error = %v", closeErr)
|
||||
}
|
||||
|
||||
err = bridge.OnEvent(context.Background(), Event{
|
||||
|
|
@ -1035,7 +1102,12 @@ func seedReadyRule(t *testing.T, workspace string) {
|
|||
}
|
||||
}
|
||||
|
||||
func newEvolutionTestLoop(t *testing.T, workspace string, evo config.EvolutionConfig, provider providers.LLMProvider) *AgentLoop {
|
||||
func newEvolutionTestLoop(
|
||||
t *testing.T,
|
||||
workspace string,
|
||||
evo config.EvolutionConfig,
|
||||
provider providers.LLMProvider,
|
||||
) *AgentLoop {
|
||||
t.Helper()
|
||||
|
||||
cfg := &config.Config{
|
||||
|
|
@ -1163,15 +1235,15 @@ func assertProfileNotExists(t *testing.T, workspace, skillName string) {
|
|||
t.Helper()
|
||||
|
||||
store := evolution.NewStore(evolution.NewPaths(workspace, ""))
|
||||
if _, err := store.LoadProfile(skillName); !os.IsNotExist(err) {
|
||||
t.Fatalf("profile %q should not exist, got err = %v", skillName, err)
|
||||
if _, loadErr := store.LoadProfile(skillName); !os.IsNotExist(loadErr) {
|
||||
t.Fatalf("profile %q should not exist, got err = %v", skillName, loadErr)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNotExists(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("%s should not exist, stat err = %v", path, err)
|
||||
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("%s should not exist, stat err = %v", path, statErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,13 @@ func (al *AgentLoop) SubscribeEvents(buffer int) EventSubscription {
|
|||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sub, in, err := al.runtimeEvents.Channel().Source("agent").OfKind(legacyAgentEventKinds()...).SubscribeChan(ctx, runtimeevents.SubscribeOptions{
|
||||
Name: "legacy-agent-events",
|
||||
Buffer: buffer,
|
||||
})
|
||||
sub, in, err := al.runtimeEvents.Channel().
|
||||
Source("agent").
|
||||
OfKind(legacyAgentEventKinds()...).
|
||||
SubscribeChan(ctx, runtimeevents.SubscribeOptions{
|
||||
Name: "legacy-agent-events",
|
||||
Buffer: buffer,
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
close(out)
|
||||
|
|
|
|||
|
|
@ -219,11 +219,11 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
|
|||
if finalContent == "" {
|
||||
finalContent = ts.opts.DefaultResponse
|
||||
}
|
||||
result, err := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent)
|
||||
if err != nil {
|
||||
result, finalizeErr := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent)
|
||||
if finalizeErr != nil {
|
||||
turnStatus = TurnEndStatusError
|
||||
}
|
||||
return result, err
|
||||
return result, finalizeErr
|
||||
case ControlToolLoop:
|
||||
// Execute tools via Pipeline
|
||||
toolCtrl := pipeline.ExecuteTools(ctx, turnCtx, ts, exec, iteration)
|
||||
|
|
@ -250,11 +250,11 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
|
|||
if exec.allResponsesHandled {
|
||||
finalContent = ""
|
||||
}
|
||||
result, err := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent)
|
||||
if err != nil {
|
||||
result, finalizeErr := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent)
|
||||
if finalizeErr != nil {
|
||||
turnStatus = TurnEndStatusError
|
||||
}
|
||||
return result, err
|
||||
return result, finalizeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -835,7 +835,8 @@ func TestTurnState_SkillContextSnapshotsTrackLatestSuccessfulPath(t *testing.T)
|
|||
ts.recordSkillContextSnapshot(skillContextTriggerInitialBuild, []string{"skill-a"})
|
||||
ts.recordSkillContextSnapshot(skillContextTriggerContextRetryRebuild, []string{"skill-b", "skill-c"})
|
||||
|
||||
if got := ts.attemptedSkillsSnapshot(); len(got) != 3 || got[0] != "skill-a" || got[1] != "skill-b" || got[2] != "skill-c" {
|
||||
if got := ts.attemptedSkillsSnapshot(); len(got) != 3 || got[0] != "skill-a" || got[1] != "skill-b" ||
|
||||
got[2] != "skill-c" {
|
||||
t.Fatalf("attemptedSkillsSnapshot = %v, want [skill-a skill-b skill-c]", got)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -170,15 +170,6 @@ func (c EvolutionConfig) EffectiveColdPathTimes() []string {
|
|||
return out
|
||||
}
|
||||
|
||||
func (c EvolutionConfig) legacyRunsColdPathAutomatically() bool {
|
||||
switch c.EffectiveMode() {
|
||||
case "draft", "apply":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c EvolutionConfig) AutoAppliesDrafts() bool {
|
||||
return c.EffectiveMode() == "apply"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,7 +333,12 @@ func TestEvolutionConfig_ColdPathTriggerMode(t *testing.T) {
|
|||
assert.True(t, (EvolutionConfig{Enabled: true, Mode: "draft"}).RunsColdPathAfterTurn())
|
||||
assert.False(t, (EvolutionConfig{Enabled: true, Mode: "draft"}).RunsColdPathScheduled())
|
||||
|
||||
scheduled := EvolutionConfig{Enabled: true, Mode: "apply", ColdPathTrigger: "scheduled", ColdPathTimes: []string{"03:00"}}
|
||||
scheduled := EvolutionConfig{
|
||||
Enabled: true,
|
||||
Mode: "apply",
|
||||
ColdPathTrigger: "scheduled",
|
||||
ColdPathTimes: []string{"03:00"},
|
||||
}
|
||||
assert.Equal(t, "scheduled", scheduled.ColdPathTriggerMode())
|
||||
assert.False(t, scheduled.RunsColdPathAfterTurn())
|
||||
assert.True(t, scheduled.RunsColdPathScheduled())
|
||||
|
|
@ -448,8 +453,8 @@ func TestSaveConfig_DisabledEvolutionOmitsApplyMode(t *testing.T) {
|
|||
}
|
||||
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
t.Fatalf("Unmarshal saved config: %v", err)
|
||||
if unmarshalErr := json.Unmarshal(data, &raw); unmarshalErr != nil {
|
||||
t.Fatalf("Unmarshal saved config: %v", unmarshalErr)
|
||||
}
|
||||
evolutionRaw, ok := raw["evolution"].(map[string]any)
|
||||
if !ok {
|
||||
|
|
@ -464,8 +469,8 @@ func TestSaveConfig_DisabledEvolutionOmitsApplyMode(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Marshal edited config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, edited, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(configPath): %v", err)
|
||||
if writeErr := os.WriteFile(configPath, edited, 0o600); writeErr != nil {
|
||||
t.Fatalf("WriteFile(configPath): %v", writeErr)
|
||||
}
|
||||
|
||||
loaded, err := LoadConfig(configPath)
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/fileutil"
|
||||
"github.com/sipeed/picoclaw/pkg/skills"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Applier struct {
|
||||
|
|
@ -37,14 +38,18 @@ func (a *Applier) ApplyDraft(ctx context.Context, workspace string, draft SkillD
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string, draft SkillDraft) (func() error, error) {
|
||||
func (a *Applier) applyDraftWithRollback(
|
||||
ctx context.Context,
|
||||
workspace string,
|
||||
draft SkillDraft,
|
||||
) (func() error, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
if err := skills.ValidateSkillName(draft.TargetSkillName); err != nil {
|
||||
return nil, err
|
||||
if validateErr := skills.ValidateSkillName(draft.TargetSkillName); validateErr != nil {
|
||||
return nil, validateErr
|
||||
}
|
||||
|
||||
existingBody, backupPath, hadOriginal, err := a.backupCurrentSkill(workspace, draft.TargetSkillName)
|
||||
|
|
@ -53,8 +58,8 @@ func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string,
|
|||
}
|
||||
|
||||
skillDir := filepath.Join(workspace, "skills", draft.TargetSkillName)
|
||||
if err := os.MkdirAll(skillDir, 0o755); err != nil {
|
||||
return nil, err
|
||||
if mkdirErr := os.MkdirAll(skillDir, 0o755); mkdirErr != nil {
|
||||
return nil, mkdirErr
|
||||
}
|
||||
|
||||
renderedBody, err := renderAppliedBody(draft, existingBody, hadOriginal)
|
||||
|
|
@ -67,7 +72,11 @@ func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateAppliedSkillBody(renderedBody, draft.TargetSkillName, allowsExistingFrontmatterFields(draft.ChangeKind, hadOriginal)); err != nil {
|
||||
if err := validateAppliedSkillBody(
|
||||
renderedBody,
|
||||
draft.TargetSkillName,
|
||||
allowsExistingFrontmatterFields(draft.ChangeKind, hadOriginal),
|
||||
); err != nil {
|
||||
if rollbackErr := a.rollbackSkill(skillPath, backupPath, hadOriginal); rollbackErr != nil {
|
||||
return nil, errorsJoin(err, rollbackErr)
|
||||
}
|
||||
|
|
@ -79,9 +88,11 @@ func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (a *Applier) backupCurrentSkill(workspace, skillName string) (currentBody, backupPath string, hadOriginal bool, err error) {
|
||||
if err := skills.ValidateSkillName(skillName); err != nil {
|
||||
return "", "", false, err
|
||||
func (a *Applier) backupCurrentSkill(
|
||||
workspace, skillName string,
|
||||
) (currentBody, backupPath string, hadOriginal bool, err error) {
|
||||
if validateErr := skills.ValidateSkillName(skillName); validateErr != nil {
|
||||
return "", "", false, validateErr
|
||||
}
|
||||
|
||||
skillPath := filepath.Join(workspace, "skills", skillName, "SKILL.md")
|
||||
|
|
@ -217,7 +228,11 @@ func renderDeployablePatchBody(body, targetSkillName string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
if name := strings.TrimSpace(fields["name"]); name != "" && name != targetSkillName {
|
||||
return "", fmt.Errorf("skill patch frontmatter name %q does not match target skill %q", name, targetSkillName)
|
||||
return "", fmt.Errorf(
|
||||
"skill patch frontmatter name %q does not match target skill %q",
|
||||
name,
|
||||
targetSkillName,
|
||||
)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(stripLeadingH1(markdownBody)), nil
|
||||
|
|
|
|||
|
|
@ -101,7 +101,10 @@ func TestApplier_CreateDraftRendersDeployableSkillWithoutLearningTrace(t *testin
|
|||
if !strings.Contains(content, "Use native-name query first.") {
|
||||
t.Fatalf("deployed skill lost procedure:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "description: Perform mathematical calculations by applying specific theorems and their associated rules.") {
|
||||
if !strings.Contains(
|
||||
content,
|
||||
"description: Perform mathematical calculations by applying specific theorems and their associated rules.",
|
||||
) {
|
||||
t.Fatalf("deployed skill did not clean description:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
|
@ -682,20 +685,23 @@ func TestApplier_BackupsAreScopedByWorkspace(t *testing.T) {
|
|||
}
|
||||
|
||||
var backupBodies []string
|
||||
if err := filepath.WalkDir(filepath.Join(sharedState, "backups"), func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() || entry.Name() != "SKILL.md" {
|
||||
if err := filepath.WalkDir(
|
||||
filepath.Join(sharedState, "backups"),
|
||||
func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() || entry.Name() != "SKILL.md" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupBodies = append(backupBodies, string(data))
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupBodies = append(backupBodies, string(data))
|
||||
return nil
|
||||
}); err != nil {
|
||||
},
|
||||
); err != nil {
|
||||
t.Fatalf("WalkDir(backups): %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import (
|
|||
)
|
||||
|
||||
type blockingColdPathRuntime struct {
|
||||
runCount atomic.Int32
|
||||
runCount atomic.Int32
|
||||
cancelCount atomic.Int32
|
||||
started chan string
|
||||
release chan struct{}
|
||||
started chan string
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (r *blockingColdPathRuntime) RunColdPathOnce(ctx context.Context, workspace string) error {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@ type DraftGenerator interface {
|
|||
}
|
||||
|
||||
type EvidenceAwareDraftGenerator interface {
|
||||
GenerateDraftWithEvidence(ctx context.Context, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) (SkillDraft, error)
|
||||
GenerateDraftWithEvidence(
|
||||
ctx context.Context,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
) (SkillDraft, error)
|
||||
}
|
||||
|
||||
type DraftEvidence struct {
|
||||
|
|
@ -72,11 +77,20 @@ func NewDefaultDraftGenerator(workspace string) *DefaultDraftGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) GenerateDraft(_ context.Context, rule LearningRecord, matches []skills.SkillInfo) (SkillDraft, error) {
|
||||
func (g *DefaultDraftGenerator) GenerateDraft(
|
||||
_ context.Context,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
) (SkillDraft, error) {
|
||||
return g.GenerateDraftWithEvidence(context.Background(), rule, matches, DraftEvidence{})
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) GenerateDraftWithEvidence(_ context.Context, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) (SkillDraft, error) {
|
||||
func (g *DefaultDraftGenerator) GenerateDraftWithEvidence(
|
||||
_ context.Context,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
) (SkillDraft, error) {
|
||||
rule = enrichRuleWithDraftEvidence(rule, evidence)
|
||||
target := inferTargetSkillName(rule, matches)
|
||||
if target == "" {
|
||||
|
|
@ -185,7 +199,9 @@ func inferCombinedSkillName(rule LearningRecord) string {
|
|||
tokens := tokenizeForEvolution(rule.Summary)
|
||||
suffix := commonWinningPathSuffix(path)
|
||||
if len(tokens) == 1 && isNumericToken(tokens[0]) && suffix != "" {
|
||||
if candidate := validSkillNameOrEmpty("calculate-" + tokens[0] + "-via-" + pluralizeSuffix(suffix)); candidate != "" {
|
||||
if candidate := validSkillNameOrEmpty(
|
||||
"calculate-" + tokens[0] + "-via-" + pluralizeSuffix(suffix),
|
||||
); candidate != "" {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
|
@ -334,8 +350,16 @@ func (g *DefaultDraftGenerator) buildHumanSummary(target string, rule LearningRe
|
|||
return fmt.Sprintf("Create %s from learned pattern: %s", target, rule.Summary)
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) buildNewSkillBody(target string, rule LearningRecord, evidence DraftEvidence, matches []skills.SkillInfo) string {
|
||||
description := fmt.Sprintf("Use this skill to %s when the task matches this workflow.", sentenceFragment(fallbackString(rule.Summary, target)))
|
||||
func (g *DefaultDraftGenerator) buildNewSkillBody(
|
||||
target string,
|
||||
rule LearningRecord,
|
||||
evidence DraftEvidence,
|
||||
matches []skills.SkillInfo,
|
||||
) string {
|
||||
description := fmt.Sprintf(
|
||||
"Use this skill to %s when the task matches this workflow.",
|
||||
sentenceFragment(fallbackString(rule.Summary, target)),
|
||||
)
|
||||
body := strings.Join([]string{
|
||||
"# " + titleCaseSkillName(target),
|
||||
"",
|
||||
|
|
@ -363,7 +387,11 @@ func (g *DefaultDraftGenerator) buildNewSkillBody(target string, rule LearningRe
|
|||
return buildSkillDocument(target, description, body)
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) buildAppendBody(rule LearningRecord, evidence DraftEvidence, matches []skills.SkillInfo) string {
|
||||
func (g *DefaultDraftGenerator) buildAppendBody(
|
||||
rule LearningRecord,
|
||||
evidence DraftEvidence,
|
||||
matches []skills.SkillInfo,
|
||||
) string {
|
||||
return strings.Join([]string{
|
||||
"## Learned Evolution",
|
||||
fmt.Sprintf("- Summary: %s", strings.TrimSpace(rule.Summary)),
|
||||
|
|
@ -420,26 +448,28 @@ func (g *DefaultDraftGenerator) learnedPatternLine(rule LearningRecord) string {
|
|||
)
|
||||
}
|
||||
if len(rule.WinningPath) > 0 {
|
||||
return fmt.Sprintf("Prefer `%s` because it was the most reliable recent path.", strings.Join(rule.WinningPath, " -> "))
|
||||
return fmt.Sprintf(
|
||||
"Prefer `%s` because it was the most reliable recent path.",
|
||||
strings.Join(rule.WinningPath, " -> "),
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf("Prefer the pattern summarized as `%s`.", strings.TrimSpace(rule.Summary))
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) winningPathLine(rule LearningRecord) string {
|
||||
if len(rule.WinningPath) == 0 {
|
||||
return "No explicit winning path was recorded."
|
||||
}
|
||||
return strings.Join(rule.WinningPath, " -> ")
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) procedureLine(rule LearningRecord, evidence DraftEvidence) string {
|
||||
if len(rule.WinningPath) > 0 {
|
||||
return fmt.Sprintf("Follow `%s`, applying the concrete operation from each source skill, then return the final result directly.", strings.Join(rule.WinningPath, " -> "))
|
||||
return fmt.Sprintf(
|
||||
"Follow `%s`, applying the concrete operation from each source skill, then return the final result directly.",
|
||||
strings.Join(rule.WinningPath, " -> "),
|
||||
)
|
||||
}
|
||||
if excerpt := firstFinalOutputExcerpt(evidence, 260); excerpt != "" {
|
||||
return "Use the same operation demonstrated by the source task result: " + excerpt
|
||||
}
|
||||
return fmt.Sprintf("Solve tasks matching `%s` using the learned successful workflow, then return the final result directly.", strings.TrimSpace(rule.Summary))
|
||||
return fmt.Sprintf(
|
||||
"Solve tasks matching `%s` using the learned successful workflow, then return the final result directly.",
|
||||
strings.TrimSpace(rule.Summary),
|
||||
)
|
||||
}
|
||||
|
||||
func (g *DefaultDraftGenerator) expectedResultLine(evidence DraftEvidence) string {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,21 @@ func TestDefaultDraftGenerator_PrefersCombinedSkillForStableMultiSkillPath(t *te
|
|||
EventCount: 3,
|
||||
SuccessRate: 1,
|
||||
}, []skills.SkillInfo{
|
||||
{Name: "three-one-theorem", Path: filepath.Join(workspace, "skills", "three-one-theorem", "SKILL.md"), Source: "workspace"},
|
||||
{Name: "four-two-theorem", Path: filepath.Join(workspace, "skills", "four-two-theorem", "SKILL.md"), Source: "workspace"},
|
||||
{Name: "five-three-theorem", Path: filepath.Join(workspace, "skills", "five-three-theorem", "SKILL.md"), Source: "workspace"},
|
||||
{
|
||||
Name: "three-one-theorem",
|
||||
Path: filepath.Join(workspace, "skills", "three-one-theorem", "SKILL.md"),
|
||||
Source: "workspace",
|
||||
},
|
||||
{
|
||||
Name: "four-two-theorem",
|
||||
Path: filepath.Join(workspace, "skills", "four-two-theorem", "SKILL.md"),
|
||||
Source: "workspace",
|
||||
},
|
||||
{
|
||||
Name: "five-three-theorem",
|
||||
Path: filepath.Join(workspace, "skills", "five-three-theorem", "SKILL.md"),
|
||||
Source: "workspace",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateDraft: %v", err)
|
||||
|
|
@ -85,7 +97,10 @@ func TestDefaultDraftGenerator_CombinedSkillIncludesEvidenceAndSourceOperations(
|
|||
if err := os.WriteFile(skillPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
matches = append(matches, skills.SkillInfo{Name: source.name, Path: skillPath, Source: "workspace", Description: "theorem helper"})
|
||||
matches = append(
|
||||
matches,
|
||||
skills.SkillInfo{Name: source.name, Path: skillPath, Source: "workspace", Description: "theorem helper"},
|
||||
)
|
||||
}
|
||||
|
||||
draft, err := generator.GenerateDraftWithEvidence(context.Background(), evolution.LearningRecord{
|
||||
|
|
|
|||
|
|
@ -35,11 +35,20 @@ func NewLLMDraftGenerator(provider providers.LLMProvider, model string, fallback
|
|||
}
|
||||
}
|
||||
|
||||
func (g *LLMDraftGenerator) GenerateDraft(ctx context.Context, rule LearningRecord, matches []skills.SkillInfo) (SkillDraft, error) {
|
||||
func (g *LLMDraftGenerator) GenerateDraft(
|
||||
ctx context.Context,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
) (SkillDraft, error) {
|
||||
return g.GenerateDraftWithEvidence(ctx, rule, matches, DraftEvidence{})
|
||||
}
|
||||
|
||||
func (g *LLMDraftGenerator) GenerateDraftWithEvidence(ctx context.Context, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) (SkillDraft, error) {
|
||||
func (g *LLMDraftGenerator) GenerateDraftWithEvidence(
|
||||
ctx context.Context,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
) (SkillDraft, error) {
|
||||
rule = enrichRuleWithDraftEvidence(rule, evidence)
|
||||
if g == nil || g.provider == nil {
|
||||
return g.generateFallback(ctx, rule, matches, evidence)
|
||||
|
|
@ -97,7 +106,11 @@ func (g *LLMDraftGenerator) generateFallback(
|
|||
return g.fallback.GenerateDraft(ctx, rule, matches)
|
||||
}
|
||||
|
||||
func (g *LLMDraftGenerator) buildPrompt(rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) string {
|
||||
func (g *LLMDraftGenerator) buildPrompt(
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
) string {
|
||||
return strings.Join([]string{
|
||||
"Generate a skill draft JSON object with these required string fields:",
|
||||
`target_skill_name, draft_type, change_kind, human_summary, body_or_patch.`,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,13 @@ func TestLLMDraftGenerator_BuildPromptIncludesMatchedSkillContent(t *testing.T)
|
|||
if err := os.MkdirAll(filepath.Dir(skillPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(skillPath, []byte("---\nname: three-one-theorem\ndescription: Add 31 then delegate\n---\n# Three One\nAdd 31 to the input, then continue with the next theorem.\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(
|
||||
skillPath,
|
||||
[]byte(
|
||||
"---\nname: three-one-theorem\ndescription: Add 31 then delegate\n---\n# Three One\nAdd 31 to the input, then continue with the next theorem.\n",
|
||||
),
|
||||
0o644,
|
||||
); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -171,13 +177,22 @@ func TestLLMDraftGenerator_BuildPromptIncludesMatchedSkillContent(t *testing.T)
|
|||
if !strings.Contains(prompt, "The YAML frontmatter must contain only name and description fields") {
|
||||
t.Fatalf("prompt missing frontmatter instruction:\n%s", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "The description field must and only describe what this skill can do and when to use it") {
|
||||
if !strings.Contains(
|
||||
prompt,
|
||||
"The description field must and only describe what this skill can do and when to use it",
|
||||
) {
|
||||
t.Fatalf("prompt missing description field instruction:\n%s", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "The deployable Markdown body should only contain what the skill is useful for and how to use it") {
|
||||
if !strings.Contains(
|
||||
prompt,
|
||||
"The deployable Markdown body should only contain what the skill is useful for and how to use it",
|
||||
) {
|
||||
t.Fatalf("prompt missing deployable body scope instruction:\n%s", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "provide detailed step-by-step instructions for the exact operation or execution process") {
|
||||
if !strings.Contains(
|
||||
prompt,
|
||||
"provide detailed step-by-step instructions for the exact operation or execution process",
|
||||
) {
|
||||
t.Fatalf("prompt missing step-by-step instruction:\n%s", prompt)
|
||||
}
|
||||
if !strings.Contains(prompt, "body_or_patch is an internal draft and review artifact") {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,23 @@ import (
|
|||
)
|
||||
|
||||
type PatternClusterer interface {
|
||||
BuildPatterns(ctx context.Context, workspace string, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string, error)
|
||||
BuildPatterns(
|
||||
ctx context.Context,
|
||||
workspace string,
|
||||
tasks []LearningRecord,
|
||||
existing []LearningRecord,
|
||||
) ([]LearningRecord, []string, error)
|
||||
}
|
||||
|
||||
type evidencePatternClusterer interface {
|
||||
BuildPatternsWithEvidence(ctx context.Context, workspace string, successfulTasks []LearningRecord, evidenceTasks []LearningRecord, existing []LearningRecord, minSuccessRatio float64) ([]LearningRecord, []string, error)
|
||||
BuildPatternsWithEvidence(
|
||||
ctx context.Context,
|
||||
workspace string,
|
||||
successfulTasks []LearningRecord,
|
||||
evidenceTasks []LearningRecord,
|
||||
existing []LearningRecord,
|
||||
minSuccessRatio float64,
|
||||
) ([]LearningRecord, []string, error)
|
||||
}
|
||||
|
||||
type HeuristicPatternClusterer struct {
|
||||
|
|
@ -37,7 +49,12 @@ func NewHeuristicPatternClusterer(minCaseCount int, now func() time.Time) *Heuri
|
|||
return &HeuristicPatternClusterer{minCaseCount: minCaseCount, now: now}
|
||||
}
|
||||
|
||||
func (c *HeuristicPatternClusterer) BuildPatterns(_ context.Context, workspace string, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string, error) {
|
||||
func (c *HeuristicPatternClusterer) BuildPatterns(
|
||||
_ context.Context,
|
||||
workspace string,
|
||||
tasks []LearningRecord,
|
||||
existing []LearningRecord,
|
||||
) ([]LearningRecord, []string, error) {
|
||||
groups := make(map[string][]LearningRecord)
|
||||
keys := make([]string, 0)
|
||||
for _, task := range tasks {
|
||||
|
|
@ -68,7 +85,15 @@ func (c *HeuristicPatternClusterer) BuildPatterns(_ context.Context, workspace s
|
|||
if !hasExisting && len(cluster) < c.minCaseCount {
|
||||
continue
|
||||
}
|
||||
pattern := buildPatternFromCluster(workspace, label, heuristicClusterSummary(label, cluster), "heuristic cluster by normalized task summary", cluster, existingPattern, c.now())
|
||||
pattern := buildPatternFromCluster(
|
||||
workspace,
|
||||
label,
|
||||
heuristicClusterSummary(label, cluster),
|
||||
"heuristic cluster by normalized task summary",
|
||||
cluster,
|
||||
existingPattern,
|
||||
c.now(),
|
||||
)
|
||||
patterns = append(patterns, pattern)
|
||||
clusteredIDs = append(clusteredIDs, collectRecordIDs(cluster)...)
|
||||
}
|
||||
|
|
@ -94,7 +119,13 @@ type llmCluster struct {
|
|||
Reason string `json:"cluster_reason"`
|
||||
}
|
||||
|
||||
func NewLLMPatternClusterer(provider providers.LLMProvider, model string, fallback PatternClusterer, minCount int, now func() time.Time) *LLMPatternClusterer {
|
||||
func NewLLMPatternClusterer(
|
||||
provider providers.LLMProvider,
|
||||
model string,
|
||||
fallback PatternClusterer,
|
||||
minCount int,
|
||||
now func() time.Time,
|
||||
) *LLMPatternClusterer {
|
||||
if fallback == nil {
|
||||
fallback = NewHeuristicPatternClusterer(minCount, now)
|
||||
}
|
||||
|
|
@ -113,7 +144,12 @@ func NewLLMPatternClusterer(provider providers.LLMProvider, model string, fallba
|
|||
}
|
||||
}
|
||||
|
||||
func (c *LLMPatternClusterer) BuildPatterns(ctx context.Context, workspace string, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string, error) {
|
||||
func (c *LLMPatternClusterer) BuildPatterns(
|
||||
ctx context.Context,
|
||||
workspace string,
|
||||
tasks []LearningRecord,
|
||||
existing []LearningRecord,
|
||||
) ([]LearningRecord, []string, error) {
|
||||
if c == nil {
|
||||
return NewHeuristicPatternClusterer(0, nil).BuildPatterns(ctx, workspace, tasks, existing)
|
||||
}
|
||||
|
|
@ -175,14 +211,30 @@ func (c *LLMPatternClusterer) BuildPatternsWithEvidence(
|
|||
fallback = NewHeuristicPatternClusterer(c.minCount, c.now)
|
||||
}
|
||||
if c.provider == nil {
|
||||
return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio)
|
||||
return buildFallbackPatternsWithEvidence(
|
||||
ctx,
|
||||
fallback,
|
||||
workspace,
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
existing,
|
||||
minSuccessRatio,
|
||||
)
|
||||
}
|
||||
model := strings.TrimSpace(c.model)
|
||||
if model == "" {
|
||||
model = strings.TrimSpace(c.provider.GetDefaultModel())
|
||||
}
|
||||
if model == "" {
|
||||
return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio)
|
||||
return buildFallbackPatternsWithEvidence(
|
||||
ctx,
|
||||
fallback,
|
||||
workspace,
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
existing,
|
||||
minSuccessRatio,
|
||||
)
|
||||
}
|
||||
if len(evidenceTasks) == 0 {
|
||||
evidenceTasks = successfulTasks
|
||||
|
|
@ -201,17 +253,48 @@ func (c *LLMPatternClusterer) BuildPatternsWithEvidence(
|
|||
},
|
||||
}, nil, model, map[string]any{"temperature": 0})
|
||||
if err != nil || resp == nil || strings.TrimSpace(resp.Content) == "" {
|
||||
return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio)
|
||||
return buildFallbackPatternsWithEvidence(
|
||||
ctx,
|
||||
fallback,
|
||||
workspace,
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
existing,
|
||||
minSuccessRatio,
|
||||
)
|
||||
}
|
||||
|
||||
payload, ok := parseLLMClusterResponse(resp.Content)
|
||||
if !ok {
|
||||
return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio)
|
||||
return buildFallbackPatternsWithEvidence(
|
||||
ctx,
|
||||
fallback,
|
||||
workspace,
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
existing,
|
||||
minSuccessRatio,
|
||||
)
|
||||
}
|
||||
if len(payload.Clusters) == 0 {
|
||||
return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio)
|
||||
return buildFallbackPatternsWithEvidence(
|
||||
ctx,
|
||||
fallback,
|
||||
workspace,
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
existing,
|
||||
minSuccessRatio,
|
||||
)
|
||||
}
|
||||
patterns, clusteredIDs := c.validateAndBuildPatternsWithEvidence(workspace, payload.Clusters, successfulTasks, evidenceTasks, existing, minSuccessRatio)
|
||||
patterns, clusteredIDs := c.validateAndBuildPatternsWithEvidence(
|
||||
workspace,
|
||||
payload.Clusters,
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
existing,
|
||||
minSuccessRatio,
|
||||
)
|
||||
return patterns, clusteredIDs, nil
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +399,12 @@ func buildFallbackPatternsWithEvidence(
|
|||
return filteredPatterns, appendUniqueStrings(nil, clusteredIDs...), nil
|
||||
}
|
||||
|
||||
func (c *LLMPatternClusterer) validateAndBuildPatterns(workspace string, clusters []llmCluster, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string) {
|
||||
func (c *LLMPatternClusterer) validateAndBuildPatterns(
|
||||
workspace string,
|
||||
clusters []llmCluster,
|
||||
tasks []LearningRecord,
|
||||
existing []LearningRecord,
|
||||
) ([]LearningRecord, []string) {
|
||||
taskByID := make(map[string]LearningRecord, len(tasks))
|
||||
for _, task := range tasks {
|
||||
taskByID[task.ID] = task
|
||||
|
|
@ -354,7 +442,15 @@ func (c *LLMPatternClusterer) validateAndBuildPatterns(workspace string, cluster
|
|||
if len(clusterTasks) == 0 {
|
||||
continue
|
||||
}
|
||||
pattern := buildPatternFromCluster(workspace, label, cluster.Summary, cluster.Reason, clusterTasks, existingPattern, c.now())
|
||||
pattern := buildPatternFromCluster(
|
||||
workspace,
|
||||
label,
|
||||
cluster.Summary,
|
||||
cluster.Reason,
|
||||
clusterTasks,
|
||||
existingPattern,
|
||||
c.now(),
|
||||
)
|
||||
patterns = append(patterns, pattern)
|
||||
clusteredIDs = append(clusteredIDs, collectRecordIDs(clusterTasks)...)
|
||||
}
|
||||
|
|
@ -420,7 +516,15 @@ func (c *LLMPatternClusterer) validateAndBuildPatternsWithEvidence(
|
|||
if !hasExisting && len(clusterSuccesses) < c.minCount {
|
||||
continue
|
||||
}
|
||||
pattern := buildPatternFromCluster(workspace, label, cluster.Summary, cluster.Reason, clusterSuccesses, existingPattern, c.now())
|
||||
pattern := buildPatternFromCluster(
|
||||
workspace,
|
||||
label,
|
||||
cluster.Summary,
|
||||
cluster.Reason,
|
||||
clusterSuccesses,
|
||||
existingPattern,
|
||||
c.now(),
|
||||
)
|
||||
patterns = append(patterns, pattern)
|
||||
clusteredIDs = append(clusteredIDs, collectRecordIDs(clusterEvidence)...)
|
||||
}
|
||||
|
|
@ -488,7 +592,12 @@ func buildPatternClusterPrompt(workspace string, tasks []LearningRecord, existin
|
|||
return string(data)
|
||||
}
|
||||
|
||||
func buildPatternFromCluster(workspace, label, summary, reason string, tasks []LearningRecord, existing LearningRecord, now time.Time) LearningRecord {
|
||||
func buildPatternFromCluster(
|
||||
workspace, label, summary, reason string,
|
||||
tasks []LearningRecord,
|
||||
existing LearningRecord,
|
||||
now time.Time,
|
||||
) LearningRecord {
|
||||
taskIDs := append([]string(nil), existing.TaskRecordIDs...)
|
||||
taskIDs = appendUniqueStrings(taskIDs, collectRecordIDs(tasks)...)
|
||||
if summary = strings.TrimSpace(summary); summary == "" {
|
||||
|
|
|
|||
|
|
@ -312,59 +312,14 @@ func TestLLMPatternClusterer_MarksAllAcceptedEvidenceClusteredButStoresSuccessfu
|
|||
content: `{"clusters":[{"label":"weather-lookup","summary":"lookup weather","task_record_ids":["task-success","task-failed"],"cluster_reason":"same weather lookup goal"}]}`,
|
||||
defaultModel: "test-model",
|
||||
}
|
||||
clusterer := evolution.NewLLMPatternClusterer(
|
||||
assertClustererMarksAllAcceptedEvidenceClustered(
|
||||
t,
|
||||
provider,
|
||||
"test-model",
|
||||
evolution.NewHeuristicPatternClusterer(1, nil),
|
||||
1,
|
||||
func() time.Time { return time.Unix(1700000000, 0).UTC() },
|
||||
"weather lookup shanghai",
|
||||
"forecast for shanghai",
|
||||
"could not complete",
|
||||
"1",
|
||||
)
|
||||
success := true
|
||||
failed := false
|
||||
successfulTasks := []evolution.LearningRecord{
|
||||
{
|
||||
ID: "task-success",
|
||||
Kind: evolution.RecordKindTask,
|
||||
WorkspaceID: "workspace-a",
|
||||
Summary: "weather lookup shanghai",
|
||||
FinalOutput: "sunny",
|
||||
Status: evolution.RecordStatus("new"),
|
||||
Success: &success,
|
||||
},
|
||||
}
|
||||
evidenceTasks := []evolution.LearningRecord{
|
||||
successfulTasks[0],
|
||||
{
|
||||
ID: "task-failed",
|
||||
Kind: evolution.RecordKindTask,
|
||||
WorkspaceID: "workspace-a",
|
||||
Summary: "forecast for shanghai",
|
||||
FinalOutput: "could not complete",
|
||||
Status: evolution.RecordStatus("new"),
|
||||
Success: &failed,
|
||||
},
|
||||
}
|
||||
|
||||
patterns, clusteredIDs, err := clusterer.BuildPatternsWithEvidence(
|
||||
context.Background(),
|
||||
"workspace-a",
|
||||
successfulTasks,
|
||||
evidenceTasks,
|
||||
nil,
|
||||
0.5,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildPatternsWithEvidence: %v", err)
|
||||
}
|
||||
if len(patterns) != 1 {
|
||||
t.Fatalf("len(patterns) = %d, want 1: %#v", len(patterns), patterns)
|
||||
}
|
||||
if got := strings.Join(patterns[0].TaskRecordIDs, ","); got != "task-success" {
|
||||
t.Fatalf("pattern TaskRecordIDs = %v, want only successful task", patterns[0].TaskRecordIDs)
|
||||
}
|
||||
if got := strings.Join(clusteredIDs, ","); got != "task-success,task-failed" {
|
||||
t.Fatalf("clusteredIDs = %v, want all accepted evidence IDs", clusteredIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testing.T) {
|
||||
|
|
@ -372,6 +327,25 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin
|
|||
content: `not-json`,
|
||||
defaultModel: "test-model",
|
||||
}
|
||||
assertClustererMarksAllAcceptedEvidenceClustered(
|
||||
t,
|
||||
provider,
|
||||
"weather lookup 100",
|
||||
"weather lookup 200",
|
||||
"partial result",
|
||||
"fallback pattern",
|
||||
)
|
||||
}
|
||||
|
||||
func assertClustererMarksAllAcceptedEvidenceClustered(
|
||||
t *testing.T,
|
||||
provider *llmClusterTestProvider,
|
||||
successSummary string,
|
||||
failedSummary string,
|
||||
failedOutput string,
|
||||
wantPatternDescription string,
|
||||
) {
|
||||
t.Helper()
|
||||
clusterer := evolution.NewLLMPatternClusterer(
|
||||
provider,
|
||||
"test-model",
|
||||
|
|
@ -386,7 +360,7 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin
|
|||
ID: "task-success",
|
||||
Kind: evolution.RecordKindTask,
|
||||
WorkspaceID: "workspace-a",
|
||||
Summary: "weather lookup 100",
|
||||
Summary: successSummary,
|
||||
FinalOutput: "sunny",
|
||||
Status: evolution.RecordStatus("new"),
|
||||
Success: &success,
|
||||
|
|
@ -398,8 +372,8 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin
|
|||
ID: "task-failed",
|
||||
Kind: evolution.RecordKindTask,
|
||||
WorkspaceID: "workspace-a",
|
||||
Summary: "weather lookup 200",
|
||||
FinalOutput: "partial result",
|
||||
Summary: failedSummary,
|
||||
FinalOutput: failedOutput,
|
||||
Status: evolution.RecordStatus("new"),
|
||||
Success: &failed,
|
||||
},
|
||||
|
|
@ -417,7 +391,7 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin
|
|||
t.Fatalf("BuildPatternsWithEvidence: %v", err)
|
||||
}
|
||||
if len(patterns) != 1 {
|
||||
t.Fatalf("len(patterns) = %d, want fallback pattern: %#v", len(patterns), patterns)
|
||||
t.Fatalf("len(patterns) = %d, want %s: %#v", len(patterns), wantPatternDescription, patterns)
|
||||
}
|
||||
if got := strings.Join(patterns[0].TaskRecordIDs, ","); got != "task-success" {
|
||||
t.Fatalf("pattern TaskRecordIDs = %v, want only successful task", patterns[0].TaskRecordIDs)
|
||||
|
|
|
|||
|
|
@ -75,11 +75,17 @@ func buildLineDiffPreview(currentBody, renderedBody string) string {
|
|||
}
|
||||
|
||||
lines := make([]string, 0, (hunkBeforeEnd-hunkBeforeStart)+(hunkAfterEnd-hunkAfterStart))
|
||||
header := []string{
|
||||
header := make([]string, 0, 3+len(lines))
|
||||
header = append(header,
|
||||
"--- current",
|
||||
"+++ rendered",
|
||||
formatUnifiedHunkHeader(hunkBeforeStart, hunkBeforeEnd-hunkBeforeStart, hunkAfterStart, hunkAfterEnd-hunkAfterStart),
|
||||
}
|
||||
formatUnifiedHunkHeader(
|
||||
hunkBeforeStart,
|
||||
hunkBeforeEnd-hunkBeforeStart,
|
||||
hunkAfterStart,
|
||||
hunkAfterEnd-hunkAfterStart,
|
||||
),
|
||||
)
|
||||
for _, line := range before[hunkBeforeStart:beforeChangeStart] {
|
||||
lines = append(lines, " "+line)
|
||||
}
|
||||
|
|
@ -96,7 +102,13 @@ func buildLineDiffPreview(currentBody, renderedBody string) string {
|
|||
}
|
||||
|
||||
func formatUnifiedHunkHeader(beforeStart, beforeCount, afterStart, afterCount int) string {
|
||||
return "@@ -" + formatUnifiedRange(beforeStart+1, beforeCount) + " +" + formatUnifiedRange(afterStart+1, afterCount) + " @@"
|
||||
return "@@ -" + formatUnifiedRange(
|
||||
beforeStart+1,
|
||||
beforeCount,
|
||||
) + " +" + formatUnifiedRange(
|
||||
afterStart+1,
|
||||
afterCount,
|
||||
) + " @@"
|
||||
}
|
||||
|
||||
func formatUnifiedRange(start, count int) string {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ func inferAvoidPatterns(rule LearningRecord) []string {
|
|||
return nil
|
||||
}
|
||||
return []string{
|
||||
"avoid starting with " + strings.Join(prefix, " -> ") + " before using " + strings.Join(rule.LateAddedSkills, " -> "),
|
||||
"avoid starting with " + strings.Join(
|
||||
prefix,
|
||||
" -> ",
|
||||
) + " before using " + strings.Join(
|
||||
rule.LateAddedSkills,
|
||||
" -> ",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,11 +278,19 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error
|
|||
admittedCount := 0
|
||||
newRuleCount := 0
|
||||
if rt.patternClusterer != nil {
|
||||
recordsForOrganizer, evidenceRecordsForOrganizer, err := rt.recordsForColdPathInputs(ctx, workspace, taskRecords)
|
||||
if err != nil {
|
||||
return err
|
||||
recordsForOrganizer, evidenceRecordsForOrganizer, inputErr := rt.recordsForColdPathInputs(
|
||||
ctx,
|
||||
workspace,
|
||||
taskRecords,
|
||||
)
|
||||
if inputErr != nil {
|
||||
return inputErr
|
||||
}
|
||||
recordsForOrganizer = rt.filterRecordsByMinSuccessRatio(workspace, evidenceRecordsForOrganizer, recordsForOrganizer)
|
||||
recordsForOrganizer = rt.filterRecordsByMinSuccessRatio(
|
||||
workspace,
|
||||
evidenceRecordsForOrganizer,
|
||||
recordsForOrganizer,
|
||||
)
|
||||
admittedCount = countTaskLearningRecords(recordsForOrganizer)
|
||||
logger.DebugCF("evolution", "Admitted task records for cold path", map[string]any{
|
||||
"workspace": workspace,
|
||||
|
|
@ -303,7 +311,12 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error
|
|||
rt.cfg.EffectiveMinSuccessRatio(),
|
||||
)
|
||||
} else {
|
||||
rules, clusteredTaskIDs, err = rt.patternClusterer.BuildPatterns(ctx, workspace, recordsForOrganizer, patternRecords)
|
||||
rules, clusteredTaskIDs, err = rt.patternClusterer.BuildPatterns(
|
||||
ctx,
|
||||
workspace,
|
||||
recordsForOrganizer,
|
||||
patternRecords,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -319,14 +332,14 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error
|
|||
})
|
||||
if len(rules) > 0 {
|
||||
merged := mergePatternRecords(patternRecords, rules, workspace)
|
||||
if err := store.MergePatternRecords(rules); err != nil {
|
||||
return err
|
||||
if mergeErr := store.MergePatternRecords(rules); mergeErr != nil {
|
||||
return mergeErr
|
||||
}
|
||||
patternRecords = merged
|
||||
}
|
||||
if len(clusteredTaskIDs) > 0 {
|
||||
if err := markTaskRecordsClustered(store, clusteredTaskIDs); err != nil {
|
||||
return err
|
||||
if markErr := markTaskRecordsClustered(store, clusteredTaskIDs); markErr != nil {
|
||||
return markErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -371,17 +384,21 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error
|
|||
}
|
||||
rule, ok := readyRuleByID[draft.SourceRecordID]
|
||||
if !ok {
|
||||
logger.DebugCF("evolution", "Skipped existing candidate draft because its source pattern is not ready", map[string]any{
|
||||
"workspace": workspace,
|
||||
"draft_id": draft.ID,
|
||||
"source_record_id": draft.SourceRecordID,
|
||||
"run_id": runID,
|
||||
})
|
||||
logger.DebugCF(
|
||||
"evolution",
|
||||
"Skipped existing candidate draft because its source pattern is not ready",
|
||||
map[string]any{
|
||||
"workspace": workspace,
|
||||
"draft_id": draft.ID,
|
||||
"source_record_id": draft.SourceRecordID,
|
||||
"run_id": runID,
|
||||
},
|
||||
)
|
||||
continue
|
||||
}
|
||||
matches, err := recaller.RecallSimilarSkills(rule)
|
||||
if err != nil {
|
||||
return err
|
||||
matches, recallErr := recaller.RecallSimilarSkills(rule)
|
||||
if recallErr != nil {
|
||||
return recallErr
|
||||
}
|
||||
draft.MatchedSkillRefs = collectSkillRefs(matches)
|
||||
var normalizationNotes []string
|
||||
|
|
@ -393,17 +410,14 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error
|
|||
draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, review.Findings...)
|
||||
changedExistingDrafts = true
|
||||
if draft.Status != DraftStatusCandidate || mode != "apply" || applier == nil {
|
||||
if err := store.SaveDrafts([]SkillDraft{draft}); err != nil {
|
||||
return err
|
||||
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
if mode != "apply" || applier == nil {
|
||||
continue
|
||||
}
|
||||
updatedDraft, err := rt.applyCandidateDraft(ctx, workspace, store, applier, draft, runID)
|
||||
if err != nil {
|
||||
return err
|
||||
updatedDraft, applyErr := rt.applyCandidateDraft(ctx, workspace, store, applier, draft, runID)
|
||||
if applyErr != nil {
|
||||
return applyErr
|
||||
}
|
||||
if updatedDraft.Status == DraftStatusAccepted {
|
||||
appliedExistingDrafts++
|
||||
|
|
@ -436,12 +450,16 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error
|
|||
}
|
||||
|
||||
if _, exists := existingBySource[rule.ID]; exists {
|
||||
logger.DebugCF("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,
|
||||
})
|
||||
logger.DebugCF(
|
||||
"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
|
||||
}
|
||||
|
||||
|
|
@ -639,35 +657,6 @@ func coldPathSuccessRatioKey(workspace string, record LearningRecord) (string, b
|
|||
return key, true
|
||||
}
|
||||
|
||||
func passesColdPathRuleFilter(record LearningRecord) bool {
|
||||
return coldPathRuleRejectReason(record) == ""
|
||||
}
|
||||
|
||||
func coldPathRuleRejectReason(record LearningRecord) string {
|
||||
if !isTaskRecordKind(record.Kind) {
|
||||
return "not a task record"
|
||||
}
|
||||
if record.Success == nil || !*record.Success {
|
||||
return "task not completed"
|
||||
}
|
||||
if record.Status != "" && record.Status != RecordStatus("new") {
|
||||
return "task already processed"
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(record.SessionKey), "heartbeat") {
|
||||
return "heartbeat session"
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(record.FinalOutput), "HEARTBEAT_OK") {
|
||||
return "heartbeat output"
|
||||
}
|
||||
if strings.TrimSpace(record.Summary) == "" {
|
||||
return "missing summary"
|
||||
}
|
||||
if strings.TrimSpace(record.FinalOutput) == "" {
|
||||
return "missing final output"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func coldPathEvidenceRejectReason(record LearningRecord) string {
|
||||
if !isTaskRecordKind(record.Kind) {
|
||||
return "not a task record"
|
||||
|
|
@ -744,7 +733,13 @@ func (rt *Runtime) applierForWorkspace(workspace string) *Applier {
|
|||
return rt.applier
|
||||
}
|
||||
|
||||
func (rt *Runtime) finalizeDraft(workspace string, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence, draft SkillDraft) SkillDraft {
|
||||
func (rt *Runtime) finalizeDraft(
|
||||
workspace string,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
draft SkillDraft,
|
||||
) SkillDraft {
|
||||
if draft.ID == "" {
|
||||
draft.ID = "draft-" + rule.ID
|
||||
}
|
||||
|
|
@ -843,7 +838,12 @@ func looksLikeSkillDocument(body string) bool {
|
|||
return strings.HasPrefix(body, "---\n") && strings.Contains(body, "\n# ")
|
||||
}
|
||||
|
||||
func synthesizeSkillDocumentFromPartialDraft(target string, draft SkillDraft, rule LearningRecord, evidence DraftEvidence) string {
|
||||
func synthesizeSkillDocumentFromPartialDraft(
|
||||
target string,
|
||||
draft SkillDraft,
|
||||
rule LearningRecord,
|
||||
evidence DraftEvidence,
|
||||
) string {
|
||||
description := strings.TrimSpace(draft.HumanSummary)
|
||||
if description == "" {
|
||||
description = fmt.Sprintf("Learned workflow for %s.", target)
|
||||
|
|
@ -876,7 +876,13 @@ func synthesizeSkillDocumentFromPartialDraft(target string, draft SkillDraft, ru
|
|||
return buildSkillDocument(target, description, body)
|
||||
}
|
||||
|
||||
func synthesizeCombinedSkillDocument(target string, draft SkillDraft, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) string {
|
||||
func synthesizeCombinedSkillDocument(
|
||||
target string,
|
||||
draft SkillDraft,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
) string {
|
||||
description := strings.TrimSpace(draft.HumanSummary)
|
||||
if description == "" {
|
||||
description = buildCombinedSkillHumanSummary(target, rule, false)
|
||||
|
|
@ -908,7 +914,13 @@ func synthesizeCombinedSkillDocument(target string, draft SkillDraft, rule Learn
|
|||
return buildSkillDocument(target, description, body)
|
||||
}
|
||||
|
||||
func synthesizeCombinedSkillAppendBody(target string, draft SkillDraft, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) string {
|
||||
func synthesizeCombinedSkillAppendBody(
|
||||
target string,
|
||||
draft SkillDraft,
|
||||
rule LearningRecord,
|
||||
matches []skills.SkillInfo,
|
||||
evidence DraftEvidence,
|
||||
) string {
|
||||
lines := []string{
|
||||
"## Learned Shortcut Update",
|
||||
fmt.Sprintf("- Shortcut skill: `%s`", target),
|
||||
|
|
@ -929,7 +941,11 @@ func synthesizeCombinedSkillAppendBody(target string, draft SkillDraft, rule Lea
|
|||
|
||||
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))
|
||||
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)
|
||||
|
|
@ -945,7 +961,11 @@ 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, " -> "))
|
||||
return fmt.Sprintf(
|
||||
"Use `%s` as a direct shortcut instead of replaying `%s` step by step.",
|
||||
target,
|
||||
strings.Join(rule.WinningPath, " -> "),
|
||||
)
|
||||
}
|
||||
|
||||
func synthesizedCombinedProcedure(matches []skills.SkillInfo, rule LearningRecord) string {
|
||||
|
|
@ -954,7 +974,10 @@ func synthesizedCombinedProcedure(matches []skills.SkillInfo, rule LearningRecor
|
|||
if len(rule.WinningPath) == 0 {
|
||||
return "Use the learned shortcut directly and keep the response focused on the requested result."
|
||||
}
|
||||
return fmt.Sprintf("Apply the recorded path `%s`, then return the final result with only the necessary explanation.", strings.Join(rule.WinningPath, " -> "))
|
||||
return fmt.Sprintf(
|
||||
"Apply the recorded path `%s`, then return the final result with only the necessary explanation.",
|
||||
strings.Join(rule.WinningPath, " -> "),
|
||||
)
|
||||
}
|
||||
return "Follow the source skill guidance below as one compact procedure, then return the final result without replaying unnecessary discovery steps."
|
||||
}
|
||||
|
|
@ -994,12 +1017,18 @@ func synthesizedWrappedPathLine(rule LearningRecord) string {
|
|||
func synthesizedCombinedLearnedContent(body string, rule LearningRecord) string {
|
||||
content := strings.TrimSpace(stripSkillFrontmatter(body))
|
||||
if content == "" {
|
||||
return fmt.Sprintf("Learned from `%s`; use this shortcut directly when the same task pattern appears again.", fallbackEvolutionSummary(rule))
|
||||
return fmt.Sprintf(
|
||||
"Learned from `%s`; use this shortcut directly when the same task pattern appears again.",
|
||||
fallbackEvolutionSummary(rule),
|
||||
)
|
||||
}
|
||||
content = removeVerboseCombinedSections(content)
|
||||
content = strings.Join(strings.Fields(content), " ")
|
||||
if content == "" {
|
||||
return fmt.Sprintf("Learned from `%s`; use this shortcut directly when the same task pattern appears again.", fallbackEvolutionSummary(rule))
|
||||
return fmt.Sprintf(
|
||||
"Learned from `%s`; use this shortcut directly when the same task pattern appears again.",
|
||||
fallbackEvolutionSummary(rule),
|
||||
)
|
||||
}
|
||||
content = trimAtReadableBoundary(content, 1200)
|
||||
return "- Learned task: " + fallbackEvolutionSummary(rule) + "\n- Reusable guidance: " + content
|
||||
|
|
@ -1275,7 +1304,8 @@ func markTaskRecordsClustered(store *Store, ids []string) error {
|
|||
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") {
|
||||
if !isPatternRecordKind(record.Kind) || record.WorkspaceID != workspace ||
|
||||
record.Status != RecordStatus("ready") {
|
||||
continue
|
||||
}
|
||||
seen[record.ID] = record
|
||||
|
|
@ -1341,7 +1371,10 @@ func (rt *Runtime) applyCandidateDraft(
|
|||
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))
|
||||
draft.ScanFindings = appendUniqueStrings(
|
||||
draft.ScanFindings,
|
||||
fmt.Sprintf("rollback audit failed: %v", auditErr),
|
||||
)
|
||||
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
|
||||
return draft, errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), auditErr, saveErr)
|
||||
}
|
||||
|
|
@ -1379,7 +1412,10 @@ func (rt *Runtime) applyCandidateDraft(
|
|||
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))
|
||||
draft.ScanFindings = appendUniqueStrings(
|
||||
draft.ScanFindings,
|
||||
fmt.Sprintf("apply rollback failed: %v", rollbackErr),
|
||||
)
|
||||
if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil {
|
||||
return draft, errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), rollbackErr, saveErr)
|
||||
}
|
||||
|
|
@ -1401,21 +1437,25 @@ func (rt *Runtime) applyCandidateDraft(
|
|||
|
||||
func (rt *Runtime) recordRollbackAudit(store *Store, draft SkillDraft, applyErr error) error {
|
||||
now := rt.now()
|
||||
return store.UpdateProfile(draft.WorkspaceID, draft.TargetSkillName, func(profile *SkillProfile, exists bool) error {
|
||||
if !exists {
|
||||
return store.UpdateProfile(
|
||||
draft.WorkspaceID,
|
||||
draft.TargetSkillName,
|
||||
func(profile *SkillProfile, exists bool) error {
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
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 nil
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func profileOrigin(origin string) string {
|
||||
|
|
|
|||
|
|
@ -62,13 +62,13 @@ func TestRuntime_RunColdPathOnce_ApplyModeWritesSkillAndProfile(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
skillPath := filepath.Join(root, "skills", "weather", "SKILL.md")
|
||||
if _, err := os.Stat(skillPath); err != nil {
|
||||
t.Fatalf("expected skill file: %v", err)
|
||||
if _, statErr := os.Stat(skillPath); statErr != nil {
|
||||
t.Fatalf("expected skill file: %v", statErr)
|
||||
}
|
||||
|
||||
profile, err := store.LoadProfile("weather")
|
||||
|
|
@ -90,7 +90,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeWritesSkillAndProfile(t *testing.T) {
|
|||
if len(profile.PreferredEntryPath) != 1 || profile.PreferredEntryPath[0] != "weather" {
|
||||
t.Fatalf("PreferredEntryPath = %v, want [weather]", profile.PreferredEntryPath)
|
||||
}
|
||||
if len(profile.AvoidPatterns) != 1 || profile.AvoidPatterns[0] != "avoid translating city names before querying weather" {
|
||||
if len(profile.AvoidPatterns) != 1 ||
|
||||
profile.AvoidPatterns[0] != "avoid translating city names before querying weather" {
|
||||
t.Fatalf("AvoidPatterns = %v, want populated metadata", profile.AvoidPatterns)
|
||||
}
|
||||
|
||||
|
|
@ -149,15 +150,15 @@ func TestRuntime_RunColdPathOnce_DraftModeKeepsCandidateDraft(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected no applied skill file, got err=%v", err)
|
||||
if _, statErr := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected no applied skill file, got err=%v", statErr)
|
||||
}
|
||||
if _, err := store.LoadProfile("weather"); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected no profile, got err=%v", err)
|
||||
if _, loadErr := store.LoadProfile("weather"); !os.IsNotExist(loadErr) {
|
||||
t.Fatalf("expected no profile, got err=%v", loadErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -218,17 +219,19 @@ func TestRuntime_RunColdPathOnce_DraftModeRefreshesExistingCandidateWithEvidence
|
|||
}}); err != nil {
|
||||
t.Fatalf("SavePatternRecords: %v", err)
|
||||
}
|
||||
if err := store.SaveDrafts([]evolution.SkillDraft{{
|
||||
ID: "draft-pattern-1",
|
||||
WorkspaceID: root,
|
||||
SourceRecordID: "pattern-1",
|
||||
TargetSkillName: "learned-skill",
|
||||
DraftType: evolution.DraftTypeShortcut,
|
||||
ChangeKind: evolution.ChangeKindCreate,
|
||||
HumanSummary: "old generic draft",
|
||||
BodyOrPatch: "---\nname: learned-skill\ndescription: old\n---\n# Learned Skill\n\nNo explicit winning path was recorded.\n",
|
||||
Status: evolution.DraftStatusCandidate,
|
||||
}}); err != nil {
|
||||
if err := store.SaveDrafts([]evolution.SkillDraft{
|
||||
{
|
||||
ID: "draft-pattern-1",
|
||||
WorkspaceID: root,
|
||||
SourceRecordID: "pattern-1",
|
||||
TargetSkillName: "learned-skill",
|
||||
DraftType: evolution.DraftTypeShortcut,
|
||||
ChangeKind: evolution.ChangeKindCreate,
|
||||
HumanSummary: "old generic draft",
|
||||
BodyOrPatch: "---\nname: learned-skill\ndescription: old\n---\n# Learned Skill\n\nNo explicit winning path was recorded.\n",
|
||||
Status: evolution.DraftStatusCandidate,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveDrafts: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -241,8 +244,8 @@ func TestRuntime_RunColdPathOnce_DraftModeRefreshesExistingCandidateWithEvidence
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -326,15 +329,15 @@ func TestRuntime_RunColdPathOnce_ApplyModeAppliesExistingCandidateDraft(t *testi
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); err != nil {
|
||||
t.Fatalf("expected existing candidate to be applied: %v", err)
|
||||
if _, statErr := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); statErr != nil {
|
||||
t.Fatalf("expected existing candidate to be applied: %v", statErr)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "skills", "unused-weather", "SKILL.md")); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected source rule to stay skipped after applying existing draft, got err=%v", err)
|
||||
if _, statErr := os.Stat(filepath.Join(root, "skills", "unused-weather", "SKILL.md")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected source rule to stay skipped after applying existing draft, got err=%v", statErr)
|
||||
}
|
||||
profile, err := store.LoadProfile("weather")
|
||||
if err != nil {
|
||||
|
|
@ -414,15 +417,15 @@ func TestRuntime_RunColdPathOnce_ApplyModeSkipsOrphanCandidateDraft(t *testing.T
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(root, "skills", "orphan-weather", "SKILL.md")); !os.IsNotExist(err) {
|
||||
t.Fatalf("orphan candidate draft should not be applied, got err=%v", err)
|
||||
if _, statErr := os.Stat(filepath.Join(root, "skills", "orphan-weather", "SKILL.md")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("orphan candidate draft should not be applied, got err=%v", statErr)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "skills", "valid-weather", "SKILL.md")); err != nil {
|
||||
t.Fatalf("expected current ready rule draft to be applied: %v", err)
|
||||
if _, statErr := os.Stat(filepath.Join(root, "skills", "valid-weather", "SKILL.md")); statErr != nil {
|
||||
t.Fatalf("expected current ready rule draft to be applied: %v", statErr)
|
||||
}
|
||||
drafts, err := store.LoadDrafts()
|
||||
if err != nil {
|
||||
|
|
@ -483,10 +486,13 @@ func TestRuntime_RunColdPathOnce_ApplyModeNormalizesExistingCombinedCandidateDra
|
|||
}
|
||||
|
||||
rt, err := evolution.NewRuntime(evolution.RuntimeOptions{
|
||||
Config: config.EvolutionConfig{Enabled: true, Mode: "apply"},
|
||||
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
||||
Store: store,
|
||||
Applier: evolution.NewApplier(evolution.NewPaths(root, ""), func() time.Time { return time.Unix(1700001000, 0).UTC() }),
|
||||
Config: config.EvolutionConfig{Enabled: true, Mode: "apply"},
|
||||
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
||||
Store: store,
|
||||
Applier: evolution.NewApplier(
|
||||
evolution.NewPaths(root, ""),
|
||||
func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
||||
),
|
||||
DraftGenerator: stubDraftGenerator{},
|
||||
Organizer: evolution.NewOrganizer(evolution.OrganizerOptions{MinCaseCount: 3, MinSuccessRate: 0.7}),
|
||||
SkillsRecaller: evolution.NewSkillsRecaller(root),
|
||||
|
|
@ -495,8 +501,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeNormalizesExistingCombinedCandidateDra
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(root, "skills", "calculate-100-via-theorems", "SKILL.md"))
|
||||
|
|
@ -510,7 +516,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeNormalizesExistingCombinedCandidateDra
|
|||
if strings.Contains(content, "Learned") || strings.Contains(content, "Source Evidence") {
|
||||
t.Fatalf("deployed skill should not expose learning traces:\n%s", content)
|
||||
}
|
||||
if strings.Contains(content, "messy raw component dump") || strings.Contains(content, "## Component Skill Breakdown") {
|
||||
if strings.Contains(content, "messy raw component dump") ||
|
||||
strings.Contains(content, "## Component Skill Breakdown") {
|
||||
t.Fatalf("expected old verbose draft content to be cleaned:\n%s", content)
|
||||
}
|
||||
|
||||
|
|
@ -585,8 +592,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeRetargetsStableMultiSkillPathIntoCombi
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
skillPath := filepath.Join(root, "skills", "calculate-100-via-theorems", "SKILL.md")
|
||||
|
|
@ -613,7 +620,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeRetargetsStableMultiSkillPathIntoCombi
|
|||
if strings.Contains(content, "Learned") || strings.Contains(content, "Source Evidence") {
|
||||
t.Fatalf("deployed skill should not expose learning traces:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "Add 31 to the input") || !strings.Contains(content, "Subtract 53 to produce the final result") {
|
||||
if !strings.Contains(content, "Add 31 to the input") ||
|
||||
!strings.Contains(content, "Subtract 53 to produce the final result") {
|
||||
t.Fatalf("missing extracted component skill content:\n%s", content)
|
||||
}
|
||||
if strings.Contains(content, "Extracted guidance") {
|
||||
|
|
@ -679,10 +687,13 @@ func TestRuntime_RunColdPathOnce_CombinedShortcutKeepsReadableLongGuidance(t *te
|
|||
}
|
||||
|
||||
rt, err := evolution.NewRuntime(evolution.RuntimeOptions{
|
||||
Config: config.EvolutionConfig{Enabled: true, Mode: "apply"},
|
||||
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
||||
Store: store,
|
||||
Applier: evolution.NewApplier(evolution.NewPaths(root, ""), func() time.Time { return time.Unix(1700001000, 0).UTC() }),
|
||||
Config: config.EvolutionConfig{Enabled: true, Mode: "apply"},
|
||||
Now: func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
||||
Store: store,
|
||||
Applier: evolution.NewApplier(
|
||||
evolution.NewPaths(root, ""),
|
||||
func() time.Time { return time.Unix(1700001000, 0).UTC() },
|
||||
),
|
||||
DraftGenerator: stubDraftGenerator{draft: evolution.SkillDraft{
|
||||
ID: "draft-1",
|
||||
WorkspaceID: root,
|
||||
|
|
@ -704,8 +715,8 @@ func TestRuntime_RunColdPathOnce_CombinedShortcutKeepsReadableLongGuidance(t *te
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(root, "skills", "calculate-with-theorem-chain-via-theorems", "SKILL.md"))
|
||||
|
|
@ -915,8 +926,8 @@ func TestRuntime_RunColdPathOnce_FirstApplyFailureDoesNotCreateGhostProfile(t *t
|
|||
t.Fatalf("error = %v, want ErrApplyDraftFailed", err)
|
||||
}
|
||||
|
||||
if _, err := store.LoadProfile("weather"); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected no profile after first apply failure, got err=%v", err)
|
||||
if _, loadErr := store.LoadProfile("weather"); !os.IsNotExist(loadErr) {
|
||||
t.Fatalf("expected no profile after first apply failure, got err=%v", loadErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -987,8 +998,8 @@ func TestRuntime_RunColdPathOnce_DraftSaveFailureRollsBackAppliedSkill(t *testin
|
|||
if _, statErr := os.Stat(skillPath); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected applied skill to be rolled back, got err=%v", statErr)
|
||||
}
|
||||
if _, err := store.LoadProfile("weather"); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected no profile after draft save failure, got err=%v", err)
|
||||
if _, loadErr := store.LoadProfile("weather"); !os.IsNotExist(loadErr) {
|
||||
t.Fatalf("expected no profile after draft save failure, got err=%v", loadErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1015,7 +1026,11 @@ func TestRuntime_RunColdPathOnce_AutoRunsLifecycleMaintenance(t *testing.T) {
|
|||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
skillPath := filepath.Join(skillDir, "SKILL.md")
|
||||
if err := os.WriteFile(skillPath, []byte("---\nname: stale-archived-skill\ndescription: stale\n---\n# Stale Archived Skill\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(
|
||||
skillPath,
|
||||
[]byte("---\nname: stale-archived-skill\ndescription: stale\n---\n# Stale Archived Skill\n"),
|
||||
0o644,
|
||||
); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
if err := store.SaveProfile(evolution.SkillProfile{
|
||||
|
|
@ -1044,8 +1059,8 @@ func TestRuntime_RunColdPathOnce_AutoRunsLifecycleMaintenance(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
activeProfile, err := store.LoadProfile("stale-active-skill")
|
||||
|
|
@ -1070,8 +1085,8 @@ func TestRuntime_RunColdPathOnce_AutoRunsLifecycleMaintenance(t *testing.T) {
|
|||
t.Fatalf("archived profile VersionHistory = %+v, want lifecycle:deleted entry", archivedProfile.VersionHistory)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(skillPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected lifecycle delete to remove skill file, stat err = %v", err)
|
||||
if _, statErr := os.Stat(skillPath); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected lifecycle delete to remove skill file, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ func TestRuntime_RunColdPathOnce_GeneratesCandidateDraft(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -244,8 +244,8 @@ func TestRuntime_RunColdPathOnce_AdmitsOnlyRecordsApprovedBySuccessJudge(t *test
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
if len(judge.calls) != 2 || judge.calls[0] != "task-rejected" || judge.calls[1] != "task-admitted" {
|
||||
|
|
@ -354,8 +354,8 @@ func TestRuntime_RunColdPathOnce_RejectsClusterBelowMinSuccessRatio(t *testing.T
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
patterns, err := store.LoadPatternRecords()
|
||||
|
|
@ -442,8 +442,8 @@ func TestRuntime_RunColdPathOnce_FallbackUsesJudgeAdjustedSuccessRatio(t *testin
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
patterns, err := store.LoadPatternRecords()
|
||||
|
|
@ -530,8 +530,8 @@ func TestRuntime_RunColdPathOnce_FallbackMarksAcceptedFailureEvidenceClustered(t
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
patterns, err := store.LoadPatternRecords()
|
||||
|
|
@ -615,11 +615,15 @@ func TestRuntime_RunColdPathOnce_DraftEvidenceDoesNotCrossWorkspaceWithDuplicate
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), workspaceA); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), workspaceA); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
if len(generator.evidence.TaskRecords) != 1 {
|
||||
t.Fatalf("evidence task count = %d, want 1: %#v", len(generator.evidence.TaskRecords), generator.evidence.TaskRecords)
|
||||
t.Fatalf(
|
||||
"evidence task count = %d, want 1: %#v",
|
||||
len(generator.evidence.TaskRecords),
|
||||
generator.evidence.TaskRecords,
|
||||
)
|
||||
}
|
||||
task := generator.evidence.TaskRecords[0]
|
||||
if task.WorkspaceID != workspaceA {
|
||||
|
|
@ -651,7 +655,9 @@ func TestRuntime_RunColdPathOnce_AdmitsSingleSkillTaskButWaitsForMinTaskCount(t
|
|||
UsedSkillNames: []string{"weather"},
|
||||
AddedSkillNames: []string{"weather"},
|
||||
ToolKinds: []string{"read_file"},
|
||||
ToolExecutions: []evolution.ToolExecutionRecord{{Name: "read_file", Success: true, SkillNames: []string{"weather"}}},
|
||||
ToolExecutions: []evolution.ToolExecutionRecord{
|
||||
{Name: "read_file", Success: true, SkillNames: []string{"weather"}},
|
||||
},
|
||||
AttemptTrail: &evolution.AttemptTrail{
|
||||
AttemptedSkills: []string{"weather"},
|
||||
FinalSuccessfulPath: []string{"weather"},
|
||||
|
|
@ -683,8 +689,8 @@ func TestRuntime_RunColdPathOnce_AdmitsSingleSkillTaskButWaitsForMinTaskCount(t
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
if len(judge.calls) != 1 || judge.calls[0] != "task-simple" {
|
||||
t.Fatalf("judge calls = %v, want [task-simple]", judge.calls)
|
||||
|
|
@ -757,8 +763,8 @@ func TestRuntime_RunColdPathOnce_RejectsTaskWhenSuccessJudgeRejects(t *testing.T
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
allRecords, err := store.LoadLearningRecords()
|
||||
|
|
@ -817,8 +823,8 @@ func TestRuntime_RunColdPathOnce_QuarantinesInvalidDraft(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -842,7 +848,11 @@ func TestRuntime_RunColdPathOnce_DoesNotWriteSkillFile(t *testing.T) {
|
|||
if err := os.MkdirAll(filepath.Dir(skillPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(skillPath, []byte("---\nname: weather\ndescription: test\n---\n# Weather"), 0o644); err != nil {
|
||||
if err := os.WriteFile(
|
||||
skillPath,
|
||||
[]byte("---\nname: weather\ndescription: test\n---\n# Weather"),
|
||||
0o644,
|
||||
); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -886,8 +896,8 @@ func TestRuntime_RunColdPathOnce_DoesNotWriteSkillFile(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(skillPath)
|
||||
|
|
@ -926,8 +936,8 @@ func TestRuntime_RunColdPathOnce_UsesDefaultDraftGenerator(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -981,8 +991,8 @@ func TestRuntime_RunColdPathOnce_UsesLLMDraftGeneratorWhenProviderAvailable(t *t
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -1028,8 +1038,8 @@ func TestRuntime_RunColdPathOnce_UsesDefaultDraftGeneratorWhenFactoryHasNoProvid
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -1090,8 +1100,8 @@ func TestRuntime_RunColdPathOnce_UsesGeneratorFactoryWorkspaceForFallback(t *tes
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
@ -1233,8 +1243,8 @@ func TestRuntime_RunColdPathOnce_RegeneratesAfterQuarantinedDraft(t *testing.T)
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.RunColdPathOnce(context.Background(), root); err != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", err)
|
||||
if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil {
|
||||
t.Fatalf("RunColdPathOnce: %v", runErr)
|
||||
}
|
||||
|
||||
drafts, err := store.LoadDrafts()
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ func TestRuntime_FinalizeTurnDisabledDoesNothing(t *testing.T) {
|
|||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, "")
|
||||
if _, err := os.Stat(paths.TaskRecords); !os.IsNotExist(err) {
|
||||
t.Fatalf("task records file should not exist, stat err = %v", err)
|
||||
if _, statErr := os.Stat(paths.TaskRecords); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("task records file should not exist, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,11 +46,11 @@ func TestRuntime_FinalizeTurnWithEmptyWorkspaceDoesNothing(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
TurnID: "turn-1",
|
||||
Status: "completed",
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,20 +63,20 @@ func TestRuntime_FinalizeTurnSkipsHeartbeat(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "heartbeat-turn",
|
||||
SessionKey: "heartbeat",
|
||||
Status: "completed",
|
||||
UserMessage: "# Heartbeat Check",
|
||||
FinalContent: "HEARTBEAT_OK",
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, "")
|
||||
if _, err := os.Stat(paths.TaskRecords); !os.IsNotExist(err) {
|
||||
t.Fatalf("heartbeat should not create task records, stat err = %v", err)
|
||||
if _, statErr := os.Stat(paths.TaskRecords); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("heartbeat should not create task records, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ func TestRuntime_FinalizeTurnWritesRecordWithOverride(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-1",
|
||||
SessionKey: "session-1",
|
||||
|
|
@ -111,11 +111,11 @@ func TestRuntime_FinalizeTurnWritesRecordWithOverride(t *testing.T) {
|
|||
{Name: "read_file", Success: true},
|
||||
},
|
||||
ActiveSkillNames: []string{"skill-a"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn first call: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn first call: %v", finalizeErr)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
WorkspaceID: "ws-explicit",
|
||||
TurnID: "turn-2",
|
||||
|
|
@ -129,8 +129,8 @@ func TestRuntime_FinalizeTurnWritesRecordWithOverride(t *testing.T) {
|
|||
{Name: "bash", Success: false, ErrorSummary: "exit status 1"},
|
||||
},
|
||||
ActiveSkillNames: []string{"skill-b"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn second call: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn second call: %v", finalizeErr)
|
||||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, override)
|
||||
|
|
@ -226,12 +226,12 @@ func TestRuntime_FinalizeTurnGeneratesUniqueTaskRecordIDsAcrossRestartedTurnSequ
|
|||
UserMessage: "summarize release notes",
|
||||
FinalContent: "done",
|
||||
}
|
||||
if err := rt.FinalizeTurn(context.Background(), input); err != nil {
|
||||
t.Fatalf("FinalizeTurn first: %v", err)
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), input); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn first: %v", finalizeErr)
|
||||
}
|
||||
input.SessionKey = "session-b"
|
||||
if err := rt.FinalizeTurn(context.Background(), input); err != nil {
|
||||
t.Fatalf("FinalizeTurn second: %v", err)
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), input); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn second: %v", finalizeErr)
|
||||
}
|
||||
|
||||
store := evolution.NewStore(evolution.NewPaths(workspace, ""))
|
||||
|
|
@ -285,24 +285,24 @@ func TestRuntime_FinalizeTurnSharedStateKeepsSkillProfilesScoped(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspaceA,
|
||||
TurnID: "turn-a",
|
||||
SessionKey: "session-a",
|
||||
Status: "completed",
|
||||
ActiveSkillNames: []string{"weather"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn(workspaceA): %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn(workspaceA): %v", finalizeErr)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspaceB,
|
||||
TurnID: "turn-b",
|
||||
SessionKey: "session-b",
|
||||
Status: "completed",
|
||||
ActiveSkillNames: []string{"weather"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn(workspaceB): %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn(workspaceB): %v", finalizeErr)
|
||||
}
|
||||
|
||||
loadedA, err := storeA.LoadProfile("weather")
|
||||
|
|
@ -347,7 +347,7 @@ func TestRuntime_FinalizeTurnWritesPotentiallyLearnableSignal(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-learnable",
|
||||
SessionKey: "session-learnable",
|
||||
|
|
@ -363,8 +363,8 @@ func TestRuntime_FinalizeTurnWritesPotentiallyLearnableSignal(t *testing.T) {
|
|||
{Sequence: 1, Trigger: "initial_build", SkillNames: []string{"geocode"}},
|
||||
{Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, "")
|
||||
|
|
@ -411,7 +411,7 @@ func TestRuntime_FinalizeTurnUsesSkillNamesFromToolExecutions(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-skill-chain",
|
||||
SessionKey: "session-skill-chain",
|
||||
|
|
@ -424,8 +424,8 @@ func TestRuntime_FinalizeTurnUsesSkillNamesFromToolExecutions(t *testing.T) {
|
|||
{Name: "read_file", Success: true, SkillNames: []string{"four-two"}},
|
||||
{Name: "read_file", Success: true, SkillNames: []string{"five-three"}},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, "")
|
||||
|
|
@ -446,7 +446,8 @@ func TestRuntime_FinalizeTurnUsesSkillNamesFromToolExecutions(t *testing.T) {
|
|||
if got := record.AddedSkillNames; len(got) != 0 {
|
||||
t.Fatalf("AddedSkillNames = %v, want empty", got)
|
||||
}
|
||||
if got := record.UsedSkillNames; len(got) != 3 || got[0] != "three-one" || got[1] != "four-two" || got[2] != "five-three" {
|
||||
if got := record.UsedSkillNames; len(got) != 3 || got[0] != "three-one" || got[1] != "four-two" ||
|
||||
got[2] != "five-three" {
|
||||
t.Fatalf("UsedSkillNames = %v, want [three-one four-two five-three]", got)
|
||||
}
|
||||
if got := record.AllLoadedSkillNames; len(got) != 0 {
|
||||
|
|
@ -464,7 +465,7 @@ func TestRuntime_FinalizeTurnPreservesUTF8WhenTruncatingChineseOutput(t *testing
|
|||
}
|
||||
|
||||
longChinese := strings.Repeat("中文输出", 500)
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-utf8",
|
||||
SessionKey: "session-utf8",
|
||||
|
|
@ -472,8 +473,8 @@ func TestRuntime_FinalizeTurnPreservesUTF8WhenTruncatingChineseOutput(t *testing
|
|||
Status: "completed",
|
||||
UserMessage: "请处理这段中文输出",
|
||||
FinalContent: longChinese,
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, "")
|
||||
|
|
@ -514,7 +515,7 @@ func TestRuntime_FinalizeTurnPrefersExplicitAttemptTrail(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-explicit-trail",
|
||||
SessionKey: "session-explicit-trail",
|
||||
|
|
@ -528,8 +529,8 @@ func TestRuntime_FinalizeTurnPrefersExplicitAttemptTrail(t *testing.T) {
|
|||
{Sequence: 1, Trigger: "initial_build", SkillNames: []string{"weather"}},
|
||||
{Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
paths := evolution.NewPaths(workspace, "")
|
||||
|
|
@ -579,15 +580,15 @@ func TestRuntime_FinalizeTurnUpdatesSkillProfileUsage(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-1",
|
||||
SessionKey: "session-1",
|
||||
AgentID: "agent-1",
|
||||
Status: "completed",
|
||||
ActiveSkillNames: []string{"skill-a", "skill-a"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
store := evolution.NewStore(evolution.NewPaths(workspace, ""))
|
||||
|
|
@ -610,66 +611,37 @@ func TestRuntime_FinalizeTurnUpdatesSkillProfileUsage(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRuntime_FinalizeTurnReactivatesColdSkill(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
now := time.Unix(1700001000, 0).UTC()
|
||||
store := evolution.NewStore(evolution.NewPaths(workspace, ""))
|
||||
|
||||
if err := store.SaveProfile(evolution.SkillProfile{
|
||||
SkillName: "skill-cold",
|
||||
WorkspaceID: workspace,
|
||||
Status: evolution.SkillStatusCold,
|
||||
Origin: "evolved",
|
||||
HumanSummary: "cold skill",
|
||||
LastUsedAt: now.Add(-24 * time.Hour),
|
||||
UseCount: 2,
|
||||
RetentionScore: 0.2,
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveProfile: %v", err)
|
||||
}
|
||||
|
||||
rt, err := evolution.NewRuntime(evolution.RuntimeOptions{
|
||||
Config: config.EvolutionConfig{Enabled: true, Mode: "observe"},
|
||||
Now: func() time.Time { return now },
|
||||
Store: store,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-cold",
|
||||
Status: "completed",
|
||||
ActiveSkillNames: []string{"skill-cold"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
}
|
||||
|
||||
profile, err := store.LoadProfile("skill-cold")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadProfile: %v", err)
|
||||
}
|
||||
if profile.Status != evolution.SkillStatusActive {
|
||||
t.Fatalf("Status = %q, want %q", profile.Status, evolution.SkillStatusActive)
|
||||
}
|
||||
assertFinalizeTurnReactivatesSkill(t, "skill-cold", evolution.SkillStatusCold, 2, 0.2, 24*time.Hour)
|
||||
}
|
||||
|
||||
func TestRuntime_FinalizeTurnReactivatesArchivedSkill(t *testing.T) {
|
||||
assertFinalizeTurnReactivatesSkill(t, "skill-archived", evolution.SkillStatusArchived, 5, 0.1, 48*time.Hour)
|
||||
}
|
||||
|
||||
func assertFinalizeTurnReactivatesSkill(
|
||||
t *testing.T,
|
||||
skillName string,
|
||||
initialStatus evolution.SkillStatus,
|
||||
useCount int,
|
||||
retentionScore float64,
|
||||
lastUsedAge time.Duration,
|
||||
) {
|
||||
t.Helper()
|
||||
workspace := t.TempDir()
|
||||
now := time.Unix(1700002000, 0).UTC()
|
||||
store := evolution.NewStore(evolution.NewPaths(workspace, ""))
|
||||
|
||||
if err := store.SaveProfile(evolution.SkillProfile{
|
||||
SkillName: "skill-archived",
|
||||
if saveErr := store.SaveProfile(evolution.SkillProfile{
|
||||
SkillName: skillName,
|
||||
WorkspaceID: workspace,
|
||||
Status: evolution.SkillStatusArchived,
|
||||
Status: initialStatus,
|
||||
Origin: "evolved",
|
||||
HumanSummary: "archived skill",
|
||||
LastUsedAt: now.Add(-48 * time.Hour),
|
||||
UseCount: 5,
|
||||
RetentionScore: 0.1,
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveProfile: %v", err)
|
||||
HumanSummary: string(initialStatus) + " skill",
|
||||
LastUsedAt: now.Add(-lastUsedAge),
|
||||
UseCount: useCount,
|
||||
RetentionScore: retentionScore,
|
||||
}); saveErr != nil {
|
||||
t.Fatalf("SaveProfile: %v", saveErr)
|
||||
}
|
||||
|
||||
rt, err := evolution.NewRuntime(evolution.RuntimeOptions{
|
||||
|
|
@ -681,16 +653,16 @@ func TestRuntime_FinalizeTurnReactivatesArchivedSkill(t *testing.T) {
|
|||
t.Fatalf("NewRuntime: %v", err)
|
||||
}
|
||||
|
||||
if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{
|
||||
Workspace: workspace,
|
||||
TurnID: "turn-archived",
|
||||
TurnID: "turn-" + skillName,
|
||||
Status: "completed",
|
||||
ActiveSkillNames: []string{"skill-archived"},
|
||||
}); err != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", err)
|
||||
ActiveSkillNames: []string{skillName},
|
||||
}); finalizeErr != nil {
|
||||
t.Fatalf("FinalizeTurn: %v", finalizeErr)
|
||||
}
|
||||
|
||||
profile, err := store.LoadProfile("skill-archived")
|
||||
profile, err := store.LoadProfile(skillName)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadProfile: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,21 @@ func TestRecallSimilarSkills_ReturnsWorkspaceSkillFirst(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
mustWriteSkill(filepath.Join(workspace, "skills"), "weather", "---\nname: weather\ndescription: weather lookup\n---\n# Weather\nUse weather queries.\n")
|
||||
mustWriteSkill(filepath.Join(globalHome, ".picoclaw", "skills"), "release", "---\nname: release\ndescription: release flow\n---\n# Release\nRelease build.\n")
|
||||
mustWriteSkill(builtinRoot, "weather-fallback", "---\nname: weather-fallback\ndescription: weather backup\n---\n# Weather Fallback\nBackup weather path.\n")
|
||||
mustWriteSkill(
|
||||
filepath.Join(workspace, "skills"),
|
||||
"weather",
|
||||
"---\nname: weather\ndescription: weather lookup\n---\n# Weather\nUse weather queries.\n",
|
||||
)
|
||||
mustWriteSkill(
|
||||
filepath.Join(globalHome, ".picoclaw", "skills"),
|
||||
"release",
|
||||
"---\nname: release\ndescription: release flow\n---\n# Release\nRelease build.\n",
|
||||
)
|
||||
mustWriteSkill(
|
||||
builtinRoot,
|
||||
"weather-fallback",
|
||||
"---\nname: weather-fallback\ndescription: weather backup\n---\n# Weather Fallback\nBackup weather path.\n",
|
||||
)
|
||||
|
||||
recaller := evolution.NewSkillsRecaller(workspace)
|
||||
matches, err := recaller.RecallSimilarSkills(evolution.LearningRecord{
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ func (s *Store) appendJSONLRecords(ctx context.Context, path string, records []L
|
|||
unlock := lockStoreFile(path)
|
||||
defer unlock()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
if mkdirErr := os.MkdirAll(filepath.Dir(path), 0o755); mkdirErr != nil {
|
||||
return mkdirErr
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
|
|
@ -277,8 +277,8 @@ func (s *Store) saveJSONLRecords(path string, records []LearningRecord) error {
|
|||
}
|
||||
|
||||
func (s *Store) saveJSONLRecordsLocked(path string, records []LearningRecord) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
if mkdirErr := os.MkdirAll(filepath.Dir(path), 0o755); mkdirErr != nil {
|
||||
return mkdirErr
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
|
@ -383,8 +383,8 @@ func (s *Store) SaveProfile(profile SkillProfile) error {
|
|||
unlock := lockStoreFile(path)
|
||||
defer unlock()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
if mkdirErr := os.MkdirAll(filepath.Dir(path), 0o755); mkdirErr != nil {
|
||||
return mkdirErr
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(profile, "", " ")
|
||||
|
|
@ -418,14 +418,14 @@ func (s *Store) UpdateProfile(
|
|||
return err
|
||||
}
|
||||
|
||||
if err := update(&profile, exists); err != nil {
|
||||
return err
|
||||
if updateErr := update(&profile, exists); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
if !exists && isZeroSkillProfile(profile) {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
if mkdirErr := os.MkdirAll(filepath.Dir(targetPath), 0o755); mkdirErr != nil {
|
||||
return mkdirErr
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(profile, "", " ")
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@ func TestStore_AppendLearningRecordsPersistsCaseAndRule(t *testing.T) {
|
|||
if loaded[1].Kind != evolution.RecordKindRule {
|
||||
t.Fatalf("loaded[1].Kind = %q, want %q", loaded[1].Kind, evolution.RecordKindRule)
|
||||
}
|
||||
if _, err := os.Stat(paths.LearningRecords); !os.IsNotExist(err) {
|
||||
t.Fatalf("legacy learning records file should not be written, stat err = %v", err)
|
||||
if _, statErr := os.Stat(paths.LearningRecords); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("legacy learning records file should not be written, stat err = %v", statErr)
|
||||
}
|
||||
if _, err := os.Stat(paths.TaskRecords); err != nil {
|
||||
t.Fatalf("task records file should exist: %v", err)
|
||||
if _, statErr := os.Stat(paths.TaskRecords); statErr != nil {
|
||||
t.Fatalf("task records file should exist: %v", statErr)
|
||||
}
|
||||
if _, err := os.Stat(paths.PatternRecords); err != nil {
|
||||
t.Fatalf("pattern records file should exist: %v", err)
|
||||
if _, statErr := os.Stat(paths.PatternRecords); statErr != nil {
|
||||
t.Fatalf("pattern records file should exist: %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +77,11 @@ func TestStore_LoadTaskRecordsMergesLegacyWhenSplitFileExists(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Marshal legacy: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(paths.RootDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
if mkdirErr := os.MkdirAll(paths.RootDir, 0o755); mkdirErr != nil {
|
||||
t.Fatalf("MkdirAll: %v", mkdirErr)
|
||||
}
|
||||
if err := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile legacy: %v", err)
|
||||
if writeErr := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); writeErr != nil {
|
||||
t.Fatalf("WriteFile legacy: %v", writeErr)
|
||||
}
|
||||
|
||||
current := evolution.LearningRecord{
|
||||
|
|
@ -92,8 +92,8 @@ func TestStore_LoadTaskRecordsMergesLegacyWhenSplitFileExists(t *testing.T) {
|
|||
Summary: "current task",
|
||||
Status: evolution.RecordStatus("new"),
|
||||
}
|
||||
if err := store.AppendTaskRecord(context.Background(), current); err != nil {
|
||||
t.Fatalf("AppendTaskRecord: %v", err)
|
||||
if appendErr := store.AppendTaskRecord(context.Background(), current); appendErr != nil {
|
||||
t.Fatalf("AppendTaskRecord: %v", appendErr)
|
||||
}
|
||||
|
||||
records, err := store.LoadTaskRecords()
|
||||
|
|
@ -126,11 +126,11 @@ func TestStore_LoadPatternRecordsMergesLegacyWhenSplitFileExists(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Marshal legacy: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(paths.RootDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
if mkdirErr := os.MkdirAll(paths.RootDir, 0o755); mkdirErr != nil {
|
||||
t.Fatalf("MkdirAll: %v", mkdirErr)
|
||||
}
|
||||
if err := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile legacy: %v", err)
|
||||
if writeErr := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); writeErr != nil {
|
||||
t.Fatalf("WriteFile legacy: %v", writeErr)
|
||||
}
|
||||
|
||||
current := evolution.LearningRecord{
|
||||
|
|
@ -141,8 +141,8 @@ func TestStore_LoadPatternRecordsMergesLegacyWhenSplitFileExists(t *testing.T) {
|
|||
Summary: "current pattern",
|
||||
Status: evolution.RecordStatus("ready"),
|
||||
}
|
||||
if err := store.AppendPatternRecords([]evolution.LearningRecord{current}); err != nil {
|
||||
t.Fatalf("AppendPatternRecords: %v", err)
|
||||
if appendErr := store.AppendPatternRecords([]evolution.LearningRecord{current}); appendErr != nil {
|
||||
t.Fatalf("AppendPatternRecords: %v", appendErr)
|
||||
}
|
||||
|
||||
records, err := store.LoadPatternRecords()
|
||||
|
|
@ -246,8 +246,8 @@ func TestStore_MergeKeepsSameRecordIDAcrossWorkspaces(t *testing.T) {
|
|||
t.Fatalf("len(loaded) = %d, want 2: %+v", len(loaded), loaded)
|
||||
}
|
||||
|
||||
if err := store.MarkTaskRecordsClustered([]string{"main-turn-1"}); err != nil {
|
||||
t.Fatalf("MarkTaskRecordsClustered: %v", err)
|
||||
if markErr := store.MarkTaskRecordsClustered([]string{"main-turn-1"}); markErr != nil {
|
||||
t.Fatalf("MarkTaskRecordsClustered: %v", markErr)
|
||||
}
|
||||
loaded, err = store.LoadTaskRecords()
|
||||
if err != nil {
|
||||
|
|
@ -409,12 +409,12 @@ func TestStore_LoadLearningRecordsIgnoresTruncatedTrailingLine(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("OpenFile: %v", err)
|
||||
}
|
||||
if _, err := f.WriteString("{\"id\":\"broken\""); err != nil {
|
||||
if _, writeErr := f.WriteString("{\"id\":\"broken\""); writeErr != nil {
|
||||
f.Close()
|
||||
t.Fatalf("WriteString: %v", err)
|
||||
t.Fatalf("WriteString: %v", writeErr)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
if closeErr := f.Close(); closeErr != nil {
|
||||
t.Fatalf("Close: %v", closeErr)
|
||||
}
|
||||
|
||||
loaded, err := store.LoadLearningRecords()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue