diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 3625858..b4795cb 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -90,6 +90,8 @@ type AgentLoop struct { autoLearners map[string]*autoLearner autonomyMu sync.Mutex autonomyBySess map[string]*autonomySession + controlConfirmMu sync.Mutex + controlConfirm map[string]pendingControlConfirmation } type taskExecutionDirectives struct { @@ -108,6 +110,12 @@ type autonomyIntent struct { focus string } +type intentDetectionOutcome struct { + matched bool + needsConfirm bool + confidence float64 +} + type autonomyIntentLLMResponse struct { Action string `json:"action"` IdleMinutes int `json:"idle_minutes"` @@ -145,6 +153,17 @@ type tokenUsageTotals struct { type tokenUsageTotalsKey struct{} +type pendingControlConfirmation struct { + intentType string + action string + idleInterval *time.Duration + focus string + interval *time.Duration + confidence float64 + requestedAt time.Time + originalInput string +} + func (sr *stageReporter) Publish(stage int, total int, status string, detail string) { if sr == nil || sr.onUpdate == nil { return @@ -295,6 +314,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers workers: make(map[string]*sessionWorker), autoLearners: make(map[string]*autoLearner), autonomyBySess: make(map[string]*autonomySession), + controlConfirm: make(map[string]pendingControlConfirmation), } // Inject recursive run logic so subagent has full tool-calling capability. @@ -801,7 +821,7 @@ func (al *AgentLoop) startAutoLearner(ctx context.Context, msg bus.InboundMessag go al.runAutoLearnerLoop(learnerCtx, msg) - return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Auto-learn is enabled: one round every %s. Use /autolearn stop to stop it.", interval.Truncate(time.Second))) + return al.naturalizeUserFacingText(ctx, fmt.Sprintf("Auto-learn is enabled: one round every %s. Tell me in natural language whenever you want to stop it.", interval.Truncate(time.Second))) } func (al *AgentLoop) runAutoLearnerLoop(ctx context.Context, msg bus.InboundMessage) { @@ -894,7 +914,7 @@ func (al *AgentLoop) autoLearnerStatus(ctx context.Context, sessionKey string) s learner, ok := al.autoLearners[sessionKey] if !ok || learner == nil { - return al.naturalizeUserFacingText(ctx, "Auto-learn is not enabled. Use /autolearn start to enable it.") + return al.naturalizeUserFacingText(ctx, "Auto-learn is not enabled.") } uptime := time.Since(learner.started).Truncate(time.Second) @@ -939,6 +959,221 @@ func shouldHandleControlIntents(msg bus.InboundMessage) bool { return !isSyntheticMessage(msg) } +func (al *AgentLoop) executeAutonomyIntent(ctx context.Context, msg bus.InboundMessage, intent autonomyIntent) string { + switch intent.action { + case "start": + idle := autonomyDefaultIdleInterval + if intent.idleInterval != nil { + idle = *intent.idleInterval + } + return al.startAutonomy(ctx, msg, idle, intent.focus) + case "clear_focus": + if al.clearAutonomyFocus(msg.SessionKey) { + return al.naturalizeUserFacingText(ctx, "Confirmed: the current focus is complete. Subsequent autonomous rounds will shift to other high-value tasks.") + } + return al.naturalizeUserFacingText(ctx, "Autonomy mode is not running, so the focus cannot be cleared.") + case "stop": + if al.stopAutonomy(msg.SessionKey) { + return al.naturalizeUserFacingText(ctx, "Autonomy mode stopped.") + } + return al.naturalizeUserFacingText(ctx, "Autonomy mode is not running.") + case "status": + return al.autonomyStatus(ctx, msg.SessionKey) + default: + return "" + } +} + +func (al *AgentLoop) executeAutoLearnIntent(ctx context.Context, msg bus.InboundMessage, intent autoLearnIntent) string { + switch intent.action { + case "start": + interval := autoLearnDefaultInterval + if intent.interval != nil { + interval = *intent.interval + } + return al.startAutoLearner(ctx, msg, interval) + case "stop": + if al.stopAutoLearner(msg.SessionKey) { + return al.naturalizeUserFacingText(ctx, "Auto-learn stopped.") + } + return al.naturalizeUserFacingText(ctx, "Auto-learn is not running.") + case "status": + return al.autoLearnerStatus(ctx, msg.SessionKey) + default: + return "" + } +} + +func (al *AgentLoop) handlePendingControlConfirmation(ctx context.Context, msg bus.InboundMessage) (bool, string) { + pending, ok := al.getPendingControlConfirmation(msg.SessionKey) + if !ok { + return false, "" + } + + if time.Since(pending.requestedAt) > 5*time.Minute { + al.clearPendingControlConfirmation(msg.SessionKey) + return false, "" + } + + decision, confident := classifyConfirmationReply(msg.Content) + if !confident { + // Do not keep stale pending state when user continues with a different task. + al.clearPendingControlConfirmation(msg.SessionKey) + return false, "" + } + + al.clearPendingControlConfirmation(msg.SessionKey) + if !decision { + return true, al.naturalizeUserFacingText(ctx, "Understood. I will not change autonomous control mode now.") + } + + switch pending.intentType { + case "autonomy": + intent := autonomyIntent{ + action: pending.action, + focus: pending.focus, + } + if pending.idleInterval != nil { + d := *pending.idleInterval + intent.idleInterval = &d + } + return true, al.executeAutonomyIntent(ctx, msg, intent) + case "autolearn": + intent := autoLearnIntent{action: pending.action} + if pending.interval != nil { + d := *pending.interval + intent.interval = &d + } + return true, al.executeAutoLearnIntent(ctx, msg, intent) + default: + return false, "" + } +} + +func classifyConfirmationReply(content string) (decision bool, confident bool) { + normalized := strings.ToLower(strings.TrimSpace(content)) + if normalized == "" { + return false, false + } + normalized = strings.NewReplacer(",", ",", "。", ".", "!", "!", "?", "?", ";", ";").Replace(normalized) + normalized = strings.Trim(normalized, " \t\r\n.,!?;:~`'\"") + + yesSet := map[string]struct{}{ + "yes": {}, "y": {}, "ok": {}, "okay": {}, "sure": {}, "confirm": {}, "go ahead": {}, "do it": {}, + "是": {}, "好的": {}, "好": {}, "可以": {}, "行": {}, "确认": {}, "继续": {}, "开始吧": {}, + } + noSet := map[string]struct{}{ + "no": {}, "n": {}, "cancel": {}, "stop": {}, "don't": {}, "do not": {}, + "不是": {}, "不用": {}, "不": {}, "先别": {}, "取消": {}, "不要": {}, + } + if _, ok := yesSet[normalized]; ok { + return true, true + } + if _, ok := noSet[normalized]; ok { + return false, true + } + return false, false +} + +func (al *AgentLoop) storePendingAutonomyConfirmation(sessionKey string, originalInput string, intent autonomyIntent, confidence float64) { + if al == nil { + return + } + pending := pendingControlConfirmation{ + intentType: "autonomy", + action: intent.action, + focus: intent.focus, + confidence: confidence, + requestedAt: time.Now(), + originalInput: strings.TrimSpace(originalInput), + } + if intent.idleInterval != nil { + d := *intent.idleInterval + pending.idleInterval = &d + } + al.controlConfirmMu.Lock() + al.controlConfirm[sessionKey] = pending + al.controlConfirmMu.Unlock() +} + +func (al *AgentLoop) storePendingAutoLearnConfirmation(sessionKey string, originalInput string, intent autoLearnIntent, confidence float64) { + if al == nil { + return + } + pending := pendingControlConfirmation{ + intentType: "autolearn", + action: intent.action, + confidence: confidence, + requestedAt: time.Now(), + originalInput: strings.TrimSpace(originalInput), + } + if intent.interval != nil { + d := *intent.interval + pending.interval = &d + } + al.controlConfirmMu.Lock() + al.controlConfirm[sessionKey] = pending + al.controlConfirmMu.Unlock() +} + +func (al *AgentLoop) clearPendingControlConfirmation(sessionKey string) { + if al == nil { + return + } + al.controlConfirmMu.Lock() + delete(al.controlConfirm, sessionKey) + al.controlConfirmMu.Unlock() +} + +func (al *AgentLoop) getPendingControlConfirmation(sessionKey string) (pendingControlConfirmation, bool) { + if al == nil { + return pendingControlConfirmation{}, false + } + al.controlConfirmMu.Lock() + defer al.controlConfirmMu.Unlock() + pending, ok := al.controlConfirm[sessionKey] + return pending, ok +} + +func (al *AgentLoop) formatAutonomyConfirmationPrompt(intent autonomyIntent) string { + switch intent.action { + case "start": + idleText := autonomyDefaultIdleInterval.Truncate(time.Second).String() + if intent.idleInterval != nil { + idleText = intent.idleInterval.Truncate(time.Second).String() + } + if strings.TrimSpace(intent.focus) != "" { + return fmt.Sprintf("I inferred that you want to enable autonomy mode (idle interval %s, focus: %s). Reply \"yes\" to confirm or \"no\" to cancel.", idleText, strings.TrimSpace(intent.focus)) + } + return fmt.Sprintf("I inferred that you want to enable autonomy mode (idle interval %s). Reply \"yes\" to confirm or \"no\" to cancel.", idleText) + case "stop": + return "I inferred that you want to stop autonomy mode. Reply \"yes\" to confirm or \"no\" to cancel." + case "status": + return "I inferred that you want autonomy status. Reply \"yes\" to confirm or \"no\" to cancel." + case "clear_focus": + return "I inferred that you want to clear the current autonomy focus. Reply \"yes\" to confirm or \"no\" to cancel." + default: + return "I inferred an autonomy control operation. Reply \"yes\" to confirm or \"no\" to cancel." + } +} + +func (al *AgentLoop) formatAutoLearnConfirmationPrompt(intent autoLearnIntent) string { + switch intent.action { + case "start": + intervalText := autoLearnDefaultInterval.Truncate(time.Second).String() + if intent.interval != nil { + intervalText = intent.interval.Truncate(time.Second).String() + } + return fmt.Sprintf("I inferred that you want to start auto-learn (interval %s). Reply \"yes\" to confirm or \"no\" to cancel.", intervalText) + case "stop": + return "I inferred that you want to stop auto-learn. Reply \"yes\" to confirm or \"no\" to cancel." + case "status": + return "I inferred that you want auto-learn status. Reply \"yes\" to confirm or \"no\" to cancel." + default: + return "I inferred an auto-learn control operation. Reply \"yes\" to confirm or \"no\" to cancel." + } +} + func withTokenUsageTotals(ctx context.Context) (context.Context, *tokenUsageTotals) { if ctx == nil { ctx = context.Background() @@ -1100,45 +1335,26 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } if controlEligible { - if intent, ok := al.detectAutonomyIntent(ctx, msg.Content); ok { - switch intent.action { - case "start": - idle := autonomyDefaultIdleInterval - if intent.idleInterval != nil { - idle = *intent.idleInterval - } - return al.startAutonomy(ctx, msg, idle, intent.focus), nil - case "clear_focus": - if al.clearAutonomyFocus(msg.SessionKey) { - return al.naturalizeUserFacingText(ctx, "Confirmed: the current focus is complete. Subsequent autonomous rounds will shift to other high-value tasks."), nil - } - return al.naturalizeUserFacingText(ctx, "Autonomy mode is not running, so the focus cannot be cleared."), nil - case "stop": - if al.stopAutonomy(msg.SessionKey) { - return al.naturalizeUserFacingText(ctx, "Autonomy mode stopped."), nil - } - return al.naturalizeUserFacingText(ctx, "Autonomy mode is not running."), nil - case "status": - return al.autonomyStatus(ctx, msg.SessionKey), nil - } + if handled, response := al.handlePendingControlConfirmation(ctx, msg); handled { + return response, nil + } + } + + if controlEligible { + if intent, outcome := al.detectAutonomyIntent(ctx, msg.Content); outcome.matched { + al.clearPendingControlConfirmation(msg.SessionKey) + return al.executeAutonomyIntent(ctx, msg, intent), nil + } else if outcome.needsConfirm { + al.storePendingAutonomyConfirmation(msg.SessionKey, msg.Content, intent, outcome.confidence) + return al.naturalizeUserFacingText(ctx, al.formatAutonomyConfirmationPrompt(intent)), nil } - if intent, ok := al.detectAutoLearnIntent(ctx, msg.Content); ok { - switch intent.action { - case "start": - interval := autoLearnDefaultInterval - if intent.interval != nil { - interval = *intent.interval - } - return al.startAutoLearner(ctx, msg, interval), nil - case "stop": - if al.stopAutoLearner(msg.SessionKey) { - return al.naturalizeUserFacingText(ctx, "Auto-learn stopped."), nil - } - return al.naturalizeUserFacingText(ctx, "Auto-learn is not running."), nil - case "status": - return al.autoLearnerStatus(ctx, msg.SessionKey), nil - } + if intent, outcome := al.detectAutoLearnIntent(ctx, msg.Content); outcome.matched { + al.clearPendingControlConfirmation(msg.SessionKey) + return al.executeAutoLearnIntent(ctx, msg, intent), nil + } else if outcome.needsConfirm { + al.storePendingAutoLearnConfirmation(msg.SessionKey, msg.Content, intent, outcome.confidence) + return al.naturalizeUserFacingText(ctx, al.formatAutoLearnConfirmationPrompt(intent)), nil } } @@ -2415,59 +2631,41 @@ func isExplicitRunCommand(content string) bool { return head == "/run" || head == "@run" } -func parseAutoLearnInterval(raw string) (time.Duration, error) { - text := strings.TrimSpace(raw) - if text == "" { - return autoLearnDefaultInterval, nil +func (al *AgentLoop) detectAutonomyIntent(ctx context.Context, content string) (autonomyIntent, intentDetectionOutcome) { + if intent, confidence, ok := al.inferAutonomyIntent(ctx, content); ok { + if confidence >= 0.75 { + return intent, intentDetectionOutcome{matched: true, confidence: confidence} + } + if confidence >= 0.45 { + return intent, intentDetectionOutcome{needsConfirm: true, confidence: confidence} + } + return autonomyIntent{}, intentDetectionOutcome{} } - if d, err := time.ParseDuration(text); err == nil { - return d, nil - } - var n int - if _, err := fmt.Sscanf(text, "%d", &n); err == nil && n > 0 { - return time.Duration(n) * time.Minute, nil - } - return 0, fmt.Errorf("invalid interval: %s (examples: 5m, 30s, 2h)", raw) + return autonomyIntent{}, intentDetectionOutcome{} } -func parseAutonomyIdleInterval(raw string) (time.Duration, error) { - text := strings.TrimSpace(raw) - if text == "" { - return autonomyDefaultIdleInterval, nil +func (al *AgentLoop) detectAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, intentDetectionOutcome) { + if intent, confidence, ok := al.inferAutoLearnIntent(ctx, content); ok { + if confidence >= 0.75 { + return intent, intentDetectionOutcome{matched: true, confidence: confidence} + } + if confidence >= 0.45 { + return intent, intentDetectionOutcome{needsConfirm: true, confidence: confidence} + } + return autoLearnIntent{}, intentDetectionOutcome{} } - if d, err := time.ParseDuration(text); err == nil { - return d, nil - } - var n int - if _, err := fmt.Sscanf(text, "%d", &n); err == nil && n > 0 { - return time.Duration(n) * time.Minute, nil - } - return 0, fmt.Errorf("invalid idle interval: %s (examples: 30m, 1h)", raw) + return autoLearnIntent{}, intentDetectionOutcome{} } -func (al *AgentLoop) detectAutonomyIntent(ctx context.Context, content string) (autonomyIntent, bool) { - if intent, ok := al.inferAutonomyIntent(ctx, content); ok { - return intent, true - } - return parseAutonomyIntent(content) -} - -func (al *AgentLoop) detectAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, bool) { - if intent, ok := al.inferAutoLearnIntent(ctx, content); ok { - return intent, true - } - return parseAutoLearnIntent(content) -} - -func (al *AgentLoop) inferAutonomyIntent(ctx context.Context, content string) (autonomyIntent, bool) { +func (al *AgentLoop) inferAutonomyIntent(ctx context.Context, content string) (autonomyIntent, float64, bool) { text := strings.TrimSpace(content) if text == "" { - return autonomyIntent{}, false + return autonomyIntent{}, 0, false } - // Avoid adding noticeable latency for very long task messages. - if len(text) > 800 { - return autonomyIntent{}, false + // Truncate long messages instead of skipping inference entirely. + if len(text) > 1200 { + text = truncate(text, 1200) } systemPrompt := al.withBootstrapPolicy(`You classify autonomy-control intent for an AI assistant. @@ -2490,28 +2688,24 @@ Rules: "temperature": 0.0, }) if err != nil || resp == nil { - return autonomyIntent{}, false + return autonomyIntent{}, 0, false } raw := extractJSONObject(resp.Content) if raw == "" { - return autonomyIntent{}, false + return autonomyIntent{}, 0, false } var parsed autonomyIntentLLMResponse if err := json.Unmarshal([]byte(raw), &parsed); err != nil { - return autonomyIntent{}, false + return autonomyIntent{}, 0, false } action := strings.ToLower(strings.TrimSpace(parsed.Action)) switch action { case "start", "stop", "status", "clear_focus": default: - return autonomyIntent{}, false - } - - if parsed.Confidence < 0.75 { - return autonomyIntent{}, false + return autonomyIntent{}, 0, false } intent := autonomyIntent{ @@ -2522,7 +2716,7 @@ Rules: d := time.Duration(parsed.IdleMinutes) * time.Minute intent.idleInterval = &d } - return intent, true + return intent, parsed.Confidence, true } func extractJSONObject(text string) string { @@ -2546,10 +2740,13 @@ func extractJSONObject(text string) string { return strings.TrimSpace(s[start : end+1]) } -func (al *AgentLoop) inferAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, bool) { +func (al *AgentLoop) inferAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, float64, bool) { text := strings.TrimSpace(content) - if text == "" || len(text) > 800 { - return autoLearnIntent{}, false + if text == "" { + return autoLearnIntent{}, 0, false + } + if len(text) > 1200 { + text = truncate(text, 1200) } systemPrompt := al.withBootstrapPolicy(`You classify auto-learning-control intent for an AI assistant. @@ -2571,27 +2768,24 @@ Rules: "temperature": 0.0, }) if err != nil || resp == nil { - return autoLearnIntent{}, false + return autoLearnIntent{}, 0, false } raw := extractJSONObject(resp.Content) if raw == "" { - return autoLearnIntent{}, false + return autoLearnIntent{}, 0, false } var parsed autoLearnIntentLLMResponse if err := json.Unmarshal([]byte(raw), &parsed); err != nil { - return autoLearnIntent{}, false + return autoLearnIntent{}, 0, false } action := strings.ToLower(strings.TrimSpace(parsed.Action)) switch action { case "start", "stop", "status": default: - return autoLearnIntent{}, false - } - if parsed.Confidence < 0.75 { - return autoLearnIntent{}, false + return autoLearnIntent{}, 0, false } intent := autoLearnIntent{action: action} @@ -2599,7 +2793,7 @@ Rules: d := time.Duration(parsed.IntervalMinutes) * time.Minute intent.interval = &d } - return intent, true + return intent, parsed.Confidence, true } func (al *AgentLoop) inferTaskExecutionDirectives(ctx context.Context, content string) (taskExecutionDirectives, bool) { @@ -2651,55 +2845,6 @@ Rules: }, true } -func parseAutonomyIntent(content string) (autonomyIntent, bool) { - text := strings.TrimSpace(content) - if text == "" { - return autonomyIntent{}, false - } - - fields := strings.Fields(strings.ToLower(text)) - if len(fields) < 2 || fields[0] != "autonomy" { - return autonomyIntent{}, false - } - intent := autonomyIntent{action: fields[1]} - if intent.action == "start" && len(fields) >= 3 { - if d, err := parseAutonomyIdleInterval(fields[2]); err == nil { - intent.idleInterval = &d - if len(fields) >= 4 { - intent.focus = strings.TrimSpace(strings.Join(strings.Fields(text)[3:], " ")) - } - } else { - intent.focus = strings.TrimSpace(strings.Join(strings.Fields(text)[2:], " ")) - } - } - switch intent.action { - case "start", "stop", "status", "clear_focus": - return intent, true - default: - return autonomyIntent{}, false - } -} - -func parseAutoLearnIntent(content string) (autoLearnIntent, bool) { - text := strings.TrimSpace(content) - fields := strings.Fields(strings.ToLower(text)) - if len(fields) < 2 || fields[0] != "autolearn" { - return autoLearnIntent{}, false - } - intent := autoLearnIntent{action: fields[1]} - if intent.action == "start" && len(fields) >= 3 { - if d, err := parseAutoLearnInterval(fields[2]); err == nil { - intent.interval = &d - } - } - switch intent.action { - case "start", "stop", "status": - return intent, true - default: - return autoLearnIntent{}, false - } -} - func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMessage) (bool, string, error) { text := strings.TrimSpace(msg.Content) if !strings.HasPrefix(text, "/") { @@ -2713,7 +2858,7 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess switch fields[0] { case "/help": - return true, "Slash commands:\n/help\n/status\n/run [--stage-report]\n/autonomy start [idle]\n/autonomy stop\n/autonomy status\n/autolearn start [interval]\n/autolearn stop\n/autolearn status\n/config get \n/config set \n/reload\n/pipeline list\n/pipeline status \n/pipeline ready ", nil + return true, "Slash commands:\n/help\n/status\n/run [--stage-report]\n/config get \n/config set \n/reload\n/pipeline list\n/pipeline status \n/pipeline ready ", nil case "/stop": return true, "Stop command is handled by queue runtime. Send /stop from your channel session to interrupt current response.", nil case "/status": @@ -2739,61 +2884,6 @@ func (al *AgentLoop) handleSlashCommand(ctx context.Context, msg bus.InboundMess cfg.Logging.Enabled, al.getConfigPathForCommands(), ), nil - case "/autolearn": - if len(fields) < 2 { - return true, al.naturalizeUserFacingText(ctx, "Usage: /autolearn start [interval] | /autolearn stop | /autolearn status"), nil - } - switch strings.ToLower(fields[1]) { - case "start": - interval := autoLearnDefaultInterval - if len(fields) >= 3 { - d, err := parseAutoLearnInterval(fields[2]) - if err != nil { - return true, "", err - } - interval = d - } - return true, al.startAutoLearner(ctx, msg, interval), nil - case "stop": - if al.stopAutoLearner(msg.SessionKey) { - return true, al.naturalizeUserFacingText(ctx, "Auto-learn stopped."), nil - } - return true, al.naturalizeUserFacingText(ctx, "Auto-learn is not running."), nil - case "status": - return true, al.autoLearnerStatus(ctx, msg.SessionKey), nil - default: - return true, al.naturalizeUserFacingText(ctx, "Usage: /autolearn start [interval] | /autolearn stop | /autolearn status"), nil - } - case "/autonomy": - if len(fields) < 2 { - return true, al.naturalizeUserFacingText(ctx, "Usage: /autonomy start [idle] | /autonomy stop | /autonomy status"), nil - } - switch strings.ToLower(fields[1]) { - case "start": - idle := autonomyDefaultIdleInterval - focus := "" - if len(fields) >= 3 { - d, err := parseAutonomyIdleInterval(fields[2]) - if err != nil { - focus = strings.Join(fields[2:], " ") - } else { - idle = d - } - } - if focus == "" && len(fields) >= 4 { - focus = strings.Join(fields[3:], " ") - } - return true, al.startAutonomy(ctx, msg, idle, focus), nil - case "stop": - if al.stopAutonomy(msg.SessionKey) { - return true, al.naturalizeUserFacingText(ctx, "Autonomy mode stopped."), nil - } - return true, al.naturalizeUserFacingText(ctx, "Autonomy mode is not running."), nil - case "status": - return true, al.autonomyStatus(ctx, msg.SessionKey), nil - default: - return true, al.naturalizeUserFacingText(ctx, "Usage: /autonomy start [idle] | /autonomy stop | /autonomy status"), nil - } case "/reload": running, err := al.triggerGatewayReloadFromAgent() if err != nil { diff --git a/pkg/agent/loop_directive_test.go b/pkg/agent/loop_directive_test.go index 2093dd1..fcb317c 100644 --- a/pkg/agent/loop_directive_test.go +++ b/pkg/agent/loop_directive_test.go @@ -2,7 +2,6 @@ package agent import ( "testing" - "time" "clawgo/pkg/bus" ) @@ -27,118 +26,15 @@ func TestParseTaskExecutionDirectives_Default(t *testing.T) { } } -func TestParseAutoLearnInterval(t *testing.T) { - d, err := parseAutoLearnInterval("5m") - if err != nil { - t.Fatalf("unexpected error: %v", err) +func TestClassifyConfirmationReply(t *testing.T) { + if ok, confident := classifyConfirmationReply("yes"); !confident || !ok { + t.Fatalf("expected yes to confirm") } - if d != 5*time.Minute { - t.Fatalf("unexpected duration: %s", d) + if ok, confident := classifyConfirmationReply("取消"); !confident || ok { + t.Fatalf("expected cancel to reject") } - - d, err = parseAutoLearnInterval("2") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if d != 2*time.Minute { - t.Fatalf("unexpected duration: %s", d) - } -} - -func TestParseAutoLearnInterval_Invalid(t *testing.T) { - if _, err := parseAutoLearnInterval("oops"); err == nil { - t.Fatalf("expected error") - } -} - -func TestParseAutoLearnIntent_FallbackCommand(t *testing.T) { - intent, ok := parseAutoLearnIntent("autolearn start 5m") - if !ok { - t.Fatalf("expected intent") - } - if intent.action != "start" { - t.Fatalf("unexpected action: %s", intent.action) - } - if intent.interval == nil || *intent.interval != 5*time.Minute { - t.Fatalf("unexpected interval: %v", intent.interval) - } -} - -func TestParseAutoLearnIntent_StopFallbackCommand(t *testing.T) { - intent, ok := parseAutoLearnIntent("autolearn stop") - if !ok { - t.Fatalf("expected intent") - } - if intent.action != "stop" { - t.Fatalf("unexpected action: %s", intent.action) - } -} - -func TestParseAutoLearnIntent_NoNaturalLanguageFallback(t *testing.T) { - if _, ok := parseAutoLearnIntent("please start auto learning"); ok { - t.Fatalf("expected no fallback match") - } -} - -func TestParseAutonomyIntent_FallbackCommand(t *testing.T) { - intent, ok := parseAutonomyIntent("autonomy start 15m log clustering") - if !ok { - t.Fatalf("expected intent") - } - if intent.action != "start" { - t.Fatalf("unexpected action: %s", intent.action) - } - if intent.idleInterval == nil || *intent.idleInterval != 15*time.Minute { - t.Fatalf("unexpected interval: %v", intent.idleInterval) - } - if intent.focus != "log clustering" { - t.Fatalf("unexpected focus: %q", intent.focus) - } -} - -func TestParseAutonomyIntent_StopFallbackCommand(t *testing.T) { - intent, ok := parseAutonomyIntent("autonomy stop") - if !ok { - t.Fatalf("expected intent") - } - if intent.action != "stop" { - t.Fatalf("unexpected action: %s", intent.action) - } -} - -func TestParseAutonomyIntent_StatusFallbackCommand(t *testing.T) { - intent, ok := parseAutonomyIntent("autonomy status") - if !ok { - t.Fatalf("expected intent") - } - if intent.action != "status" { - t.Fatalf("unexpected action: %s", intent.action) - } -} - -func TestParseAutonomyIdleInterval(t *testing.T) { - d, err := parseAutonomyIdleInterval("45m") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if d != 45*time.Minute { - t.Fatalf("unexpected duration: %s", d) - } -} - -func TestParseAutonomyIntent_NoNaturalLanguageFallback(t *testing.T) { - if intent, ok := parseAutonomyIntent("please run this task automatically"); ok { - t.Fatalf("expected no intent, got: %+v", intent) - } -} - -func TestParseAutonomyIntent_ClearFocusFallbackCommand(t *testing.T) { - intent, ok := parseAutonomyIntent("autonomy clear_focus") - if !ok { - t.Fatalf("expected intent") - } - if intent.action != "clear_focus" { - t.Fatalf("unexpected action: %s", intent.action) + if _, confident := classifyConfirmationReply("继续处理日志问题,不是这个"); confident { + t.Fatalf("expected non-confirmation sentence to be non-confident") } }