mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 22:09:37 +08:00
fix loop
This commit is contained in:
@@ -109,14 +109,6 @@ type controlPolicy struct {
|
||||
autoLearnMaxRoundsWithoutUser int
|
||||
}
|
||||
|
||||
type runControlLexicon struct {
|
||||
latestKeywords []string
|
||||
waitKeywords []string
|
||||
statusKeywords []string
|
||||
runMentionKeywords []string
|
||||
minuteUnits map[string]struct{}
|
||||
}
|
||||
|
||||
type runtimeControlStats struct {
|
||||
runAccepted int64
|
||||
runCompleted int64
|
||||
@@ -204,7 +196,6 @@ type AgentLoop struct {
|
||||
controlConfirmMu sync.Mutex
|
||||
controlConfirm map[string]pendingControlConfirmation
|
||||
controlPolicy controlPolicy
|
||||
runControlLex runControlLexicon
|
||||
parallelSafeTools map[string]struct{}
|
||||
maxParallelCalls int
|
||||
controlStats runtimeControlStats
|
||||
@@ -263,12 +254,15 @@ type taskExecutionDirectivesLLMResponse struct {
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
var runIDPattern = regexp.MustCompile(`(?i)\b(run-\d+-\d+)\b`)
|
||||
var runWaitTimeoutPattern = regexp.MustCompile(`(?i)(\d+)\s*(seconds|second|secs|sec|minutes|minute|mins|min|分钟|秒|s|m)`)
|
||||
var defaultRunControlLatestKeywords = []string{"latest", "last run", "recent run", "最新", "最近", "上一次", "上个"}
|
||||
var defaultRunControlWaitKeywords = []string{"wait", "等待", "等到", "阻塞"}
|
||||
var defaultRunControlStatusKeywords = []string{"status", "状态", "进度", "running", "运行"}
|
||||
var defaultRunControlRunMentionKeywords = []string{"run", "任务"}
|
||||
type runControlIntentLLMResponse struct {
|
||||
Matched bool `json:"matched"`
|
||||
RunID string `json:"run_id"`
|
||||
Latest bool `json:"latest"`
|
||||
Wait bool `json:"wait"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
var defaultParallelSafeToolNames = []string{"read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"}
|
||||
var autonomyIntentKeywords = []string{
|
||||
"autonomy", "autonomous", "autonomy mode", "self-driven", "self driven",
|
||||
@@ -278,14 +272,6 @@ var autoLearnIntentKeywords = []string{
|
||||
"auto-learn", "autolearn", "learning loop", "learn loop",
|
||||
"自学习", "学习循环", "自动学习", "学习模式",
|
||||
}
|
||||
var defaultRunWaitMinuteUnits = map[string]struct{}{
|
||||
"分钟": {},
|
||||
"min": {},
|
||||
"mins": {},
|
||||
"minute": {},
|
||||
"minutes": {},
|
||||
"m": {},
|
||||
}
|
||||
|
||||
type stageReporter struct {
|
||||
onUpdate func(content string)
|
||||
@@ -375,18 +361,9 @@ func loadControlPolicyFromConfig(base controlPolicy, rc config.RuntimeControlCon
|
||||
if rc.IntentHighConfidence > 0 {
|
||||
p.intentHighConfidence = rc.IntentHighConfidence
|
||||
}
|
||||
if rc.IntentConfirmMinConfidence >= 0 {
|
||||
p.intentConfirmMinConfidence = rc.IntentConfirmMinConfidence
|
||||
}
|
||||
if rc.IntentMaxInputChars > 0 {
|
||||
p.intentMaxInputChars = rc.IntentMaxInputChars
|
||||
}
|
||||
if rc.ConfirmTTLSeconds > 0 {
|
||||
p.confirmTTL = time.Duration(rc.ConfirmTTLSeconds) * time.Second
|
||||
}
|
||||
if rc.ConfirmMaxClarificationTurns >= 0 {
|
||||
p.confirmMaxClarificationTurns = rc.ConfirmMaxClarificationTurns
|
||||
}
|
||||
if rc.AutonomyTickIntervalSec > 0 {
|
||||
p.autonomyTickInterval = time.Duration(rc.AutonomyTickIntervalSec) * time.Second
|
||||
}
|
||||
@@ -423,42 +400,6 @@ func loadRunStatePolicyFromConfig(rc config.RuntimeControlConfig) (time.Duration
|
||||
return ttl, maxEntries
|
||||
}
|
||||
|
||||
func defaultRunControlLexicon() runControlLexicon {
|
||||
latest := append([]string(nil), defaultRunControlLatestKeywords...)
|
||||
wait := append([]string(nil), defaultRunControlWaitKeywords...)
|
||||
status := append([]string(nil), defaultRunControlStatusKeywords...)
|
||||
mention := append([]string(nil), defaultRunControlRunMentionKeywords...)
|
||||
minutes := make(map[string]struct{}, len(defaultRunWaitMinuteUnits))
|
||||
for unit := range defaultRunWaitMinuteUnits {
|
||||
minutes[unit] = struct{}{}
|
||||
}
|
||||
return runControlLexicon{
|
||||
latestKeywords: latest,
|
||||
waitKeywords: wait,
|
||||
statusKeywords: status,
|
||||
runMentionKeywords: mention,
|
||||
minuteUnits: minutes,
|
||||
}
|
||||
}
|
||||
|
||||
func loadRunControlLexiconFromConfig(rc config.RuntimeControlConfig) runControlLexicon {
|
||||
base := defaultRunControlLexicon()
|
||||
base.latestKeywords = normalizeKeywordList(rc.RunControlLatestKeywords, base.latestKeywords)
|
||||
base.waitKeywords = normalizeKeywordList(rc.RunControlWaitKeywords, base.waitKeywords)
|
||||
base.statusKeywords = normalizeKeywordList(rc.RunControlStatusKeywords, base.statusKeywords)
|
||||
base.runMentionKeywords = normalizeKeywordList(rc.RunControlRunMentionKeywords, base.runMentionKeywords)
|
||||
|
||||
minuteUnits := normalizeKeywordList(rc.RunControlMinuteUnits, nil)
|
||||
if len(minuteUnits) == 0 {
|
||||
return base
|
||||
}
|
||||
base.minuteUnits = make(map[string]struct{}, len(minuteUnits))
|
||||
for _, unit := range minuteUnits {
|
||||
base.minuteUnits[unit] = struct{}{}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func normalizeKeywordList(values []string, fallback []string) []string {
|
||||
if len(values) == 0 {
|
||||
return append([]string(nil), fallback...)
|
||||
@@ -688,7 +629,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
defaultModel := defaultModelFromModels(modelsByProxy[primaryProxy], provider)
|
||||
policy := loadControlPolicyFromConfig(defaultControlPolicy(), cfg.Agents.Defaults.RuntimeControl)
|
||||
policy = applyLegacyControlPolicyEnvOverrides(policy)
|
||||
runControlLex := loadRunControlLexiconFromConfig(cfg.Agents.Defaults.RuntimeControl)
|
||||
parallelSafeTools, maxParallelCalls := loadToolParallelPolicyFromConfig(cfg.Agents.Defaults.RuntimeControl)
|
||||
runStateTTL, runStateMax := loadRunStatePolicyFromConfig(cfg.Agents.Defaults.RuntimeControl)
|
||||
// Keep compatibility with older env names.
|
||||
@@ -722,7 +662,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
autonomyBySess: make(map[string]*autonomySession),
|
||||
controlConfirm: make(map[string]pendingControlConfirmation),
|
||||
controlPolicy: policy,
|
||||
runControlLex: runControlLex,
|
||||
parallelSafeTools: parallelSafeTools,
|
||||
maxParallelCalls: maxParallelCalls,
|
||||
runStates: make(map[string]*runState),
|
||||
@@ -731,10 +670,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
}
|
||||
logger.InfoCF("agent", "Control policy initialized", map[string]interface{}{
|
||||
"intent_high_confidence": policy.intentHighConfidence,
|
||||
"intent_confirm_min_confidence": policy.intentConfirmMinConfidence,
|
||||
"intent_max_input_chars": policy.intentMaxInputChars,
|
||||
"confirm_ttl": policy.confirmTTL.String(),
|
||||
"confirm_max_clarification_turns": policy.confirmMaxClarificationTurns,
|
||||
"autonomy_tick_interval": policy.autonomyTickInterval.String(),
|
||||
"autonomy_min_run_interval": policy.autonomyMinRunInterval.String(),
|
||||
"autonomy_idle_threshold": policy.autonomyIdleThreshold.String(),
|
||||
@@ -742,11 +678,6 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
"autonomy_max_pending_duration": policy.autonomyMaxPendingDuration.String(),
|
||||
"autonomy_max_consecutive_stalls": policy.autonomyMaxConsecutiveStalls,
|
||||
"autolearn_max_rounds_without_user": policy.autoLearnMaxRoundsWithoutUser,
|
||||
"run_control_latest_keywords": len(runControlLex.latestKeywords),
|
||||
"run_control_wait_keywords": len(runControlLex.waitKeywords),
|
||||
"run_control_status_keywords": len(runControlLex.statusKeywords),
|
||||
"run_control_run_keywords": len(runControlLex.runMentionKeywords),
|
||||
"run_control_minute_units": len(runControlLex.minuteUnits),
|
||||
"parallel_safe_tool_count": len(parallelSafeTools),
|
||||
"tool_max_parallel_calls": maxParallelCalls,
|
||||
"run_state_ttl": runStateTTL.String(),
|
||||
@@ -1697,63 +1628,9 @@ func (al *AgentLoop) waitForRun(ctx context.Context, runID string) (runState, bo
|
||||
}
|
||||
}
|
||||
|
||||
func detectRunControlIntent(content string) (runControlIntent, bool) {
|
||||
return detectRunControlIntentWithLexicon(content, defaultRunControlLexicon())
|
||||
}
|
||||
|
||||
func detectRunControlIntentWithLexicon(content string, lex runControlLexicon) (runControlIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" {
|
||||
return runControlIntent{}, false
|
||||
}
|
||||
if strings.HasPrefix(text, "/") {
|
||||
return runControlIntent{}, false
|
||||
}
|
||||
|
||||
lower := strings.ToLower(text)
|
||||
intent := runControlIntent{
|
||||
timeout: defaultRunWaitTimeout,
|
||||
}
|
||||
if m := runIDPattern.FindStringSubmatch(text); len(m) > 1 {
|
||||
intent.runID = strings.ToLower(strings.TrimSpace(m[1]))
|
||||
}
|
||||
intent.latest = containsAnySubstring(lower, lex.latestKeywords...)
|
||||
intent.wait = containsAnySubstring(lower, lex.waitKeywords...)
|
||||
isStatusQuery := containsAnySubstring(lower, lex.statusKeywords...)
|
||||
isRunMentioned := containsAnySubstring(lower, lex.runMentionKeywords...)
|
||||
if !intent.wait && !isStatusQuery {
|
||||
if intent.runID == "" || !isRunMentioned {
|
||||
return runControlIntent{}, false
|
||||
}
|
||||
}
|
||||
if intent.runID == "" && !intent.latest {
|
||||
return runControlIntent{}, false
|
||||
}
|
||||
if intent.wait {
|
||||
intent.timeout = parseRunWaitTimeoutWithLexicon(text, lex)
|
||||
}
|
||||
return intent, true
|
||||
}
|
||||
|
||||
func parseRunWaitTimeout(content string) time.Duration {
|
||||
return parseRunWaitTimeoutWithLexicon(content, defaultRunControlLexicon())
|
||||
}
|
||||
|
||||
func parseRunWaitTimeoutWithLexicon(content string, lex runControlLexicon) time.Duration {
|
||||
timeout := defaultRunWaitTimeout
|
||||
matches := runWaitTimeoutPattern.FindStringSubmatch(content)
|
||||
if len(matches) < 3 {
|
||||
return timeout
|
||||
}
|
||||
n, err := strconv.Atoi(matches[1])
|
||||
if err != nil || n <= 0 {
|
||||
return timeout
|
||||
}
|
||||
unit := strings.ToLower(strings.TrimSpace(matches[2]))
|
||||
if _, isMinute := lex.minuteUnits[unit]; isMinute {
|
||||
timeout = time.Duration(n) * time.Minute
|
||||
} else {
|
||||
timeout = time.Duration(n) * time.Second
|
||||
func normalizeRunWaitTimeout(timeout time.Duration) time.Duration {
|
||||
if timeout <= 0 {
|
||||
return defaultRunWaitTimeout
|
||||
}
|
||||
if timeout < minRunWaitTimeout {
|
||||
return minRunWaitTimeout
|
||||
@@ -1887,19 +1764,86 @@ func (al *AgentLoop) executeRunControlIntent(ctx context.Context, sessionKey str
|
||||
return al.naturalizeUserFacingText(ctx, formatRunStateReport(rs))
|
||||
}
|
||||
|
||||
func (al *AgentLoop) inferRunControlIntent(ctx context.Context, content string) (runControlIntent, float64, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" || strings.HasPrefix(text, "/") {
|
||||
return runControlIntent{}, 0, false
|
||||
}
|
||||
|
||||
limit := defaultControlPolicy().intentMaxInputChars
|
||||
if al != nil && al.controlPolicy.intentMaxInputChars > 0 {
|
||||
limit = al.controlPolicy.intentMaxInputChars
|
||||
}
|
||||
if len(text) > limit {
|
||||
text = truncate(text, limit)
|
||||
}
|
||||
|
||||
systemPrompt := al.withBootstrapPolicy(`You classify run-control intent for an AI assistant.
|
||||
Return JSON only.
|
||||
Schema:
|
||||
{"matched":true|false,"run_id":"","latest":false,"wait":false,"timeout_seconds":0,"confidence":0.0}
|
||||
Rules:
|
||||
- matched=true only when user asks run status/wait/latest-run control.
|
||||
- run_id: use canonical form like run-123-1 if provided, else empty.
|
||||
- latest=true when user asks latest/recent run.
|
||||
- wait=true when user asks to wait until run completes.
|
||||
- timeout_seconds: wait timeout in seconds, 0 means default.
|
||||
- confidence: 0..1`)
|
||||
|
||||
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: text},
|
||||
}, nil, map[string]interface{}{
|
||||
"max_tokens": 220,
|
||||
"temperature": 0.0,
|
||||
})
|
||||
if err != nil || resp == nil {
|
||||
return runControlIntent{}, 0, false
|
||||
}
|
||||
|
||||
raw := extractJSONObject(resp.Content)
|
||||
if raw == "" {
|
||||
return runControlIntent{}, 0, false
|
||||
}
|
||||
|
||||
var parsed runControlIntentLLMResponse
|
||||
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
||||
return runControlIntent{}, 0, false
|
||||
}
|
||||
if !parsed.Matched {
|
||||
return runControlIntent{}, parsed.Confidence, false
|
||||
}
|
||||
|
||||
intent := runControlIntent{
|
||||
runID: strings.ToLower(strings.TrimSpace(parsed.RunID)),
|
||||
latest: parsed.Latest,
|
||||
wait: parsed.Wait,
|
||||
timeout: defaultRunWaitTimeout,
|
||||
}
|
||||
if parsed.TimeoutSeconds > 0 {
|
||||
intent.timeout = time.Duration(parsed.TimeoutSeconds) * time.Second
|
||||
}
|
||||
intent.timeout = normalizeRunWaitTimeout(intent.timeout)
|
||||
|
||||
if intent.runID == "" && !intent.latest {
|
||||
return runControlIntent{}, parsed.Confidence, false
|
||||
}
|
||||
return intent, parsed.Confidence, true
|
||||
}
|
||||
|
||||
func (al *AgentLoop) handleNaturalRunControl(ctx context.Context, msg bus.InboundMessage) (bool, string) {
|
||||
intent, ok := detectRunControlIntentWithLexicon(msg.Content, al.effectiveRunControlLexicon())
|
||||
intent, confidence, ok := al.inferRunControlIntent(ctx, msg.Content)
|
||||
if !ok {
|
||||
return false, ""
|
||||
}
|
||||
return true, al.executeRunControlIntent(ctx, msg.SessionKey, intent)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) effectiveRunControlLexicon() runControlLexicon {
|
||||
if al == nil || len(al.runControlLex.latestKeywords) == 0 || len(al.runControlLex.minuteUnits) == 0 {
|
||||
return defaultRunControlLexicon()
|
||||
policy := defaultControlPolicy()
|
||||
if al != nil {
|
||||
policy = al.controlPolicy
|
||||
}
|
||||
return al.runControlLex
|
||||
if confidence < policy.intentHighConfidence {
|
||||
return false, ""
|
||||
}
|
||||
return true, al.executeRunControlIntent(ctx, msg.SessionKey, intent)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) controlMetricAdd(counter *int64, delta int64) {
|
||||
@@ -2025,16 +1969,16 @@ func (al *AgentLoop) handleControlPlane(ctx context.Context, msg bus.InboundMess
|
||||
al.clearPendingControlConfirmation(msg.SessionKey)
|
||||
return true, al.executeAutonomyIntent(ctx, msg, intent), nil
|
||||
} else if outcome.needsConfirm {
|
||||
al.storePendingAutonomyConfirmation(msg.SessionKey, msg.Content, intent, outcome.confidence)
|
||||
return true, al.naturalizeUserFacingText(ctx, al.formatAutonomyConfirmationPrompt(intent)), nil
|
||||
al.clearPendingControlConfirmation(msg.SessionKey)
|
||||
return true, al.executeAutonomyIntent(ctx, msg, intent), nil
|
||||
}
|
||||
|
||||
if intent, outcome := al.detectAutoLearnIntent(ctx, msg.Content); outcome.matched {
|
||||
al.clearPendingControlConfirmation(msg.SessionKey)
|
||||
return true, al.executeAutoLearnIntent(ctx, msg, intent), nil
|
||||
} else if outcome.needsConfirm {
|
||||
al.storePendingAutoLearnConfirmation(msg.SessionKey, msg.Content, intent, outcome.confidence)
|
||||
return true, al.naturalizeUserFacingText(ctx, al.formatAutoLearnConfirmationPrompt(intent)), nil
|
||||
al.clearPendingControlConfirmation(msg.SessionKey)
|
||||
return true, al.executeAutoLearnIntent(ctx, msg, intent), nil
|
||||
}
|
||||
|
||||
return false, "", nil
|
||||
|
||||
@@ -11,85 +11,35 @@ import (
|
||||
func TestDetectRunControlIntent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
intent, ok := detectRunControlIntent("请等待 run-123-7 120 秒后告诉我状态")
|
||||
if !ok {
|
||||
t.Fatalf("expected run control intent")
|
||||
}
|
||||
if intent.runID != "run-123-7" {
|
||||
t.Fatalf("unexpected run id: %s", intent.runID)
|
||||
}
|
||||
if !intent.wait {
|
||||
t.Fatalf("expected wait=true")
|
||||
}
|
||||
if intent.timeout != 120*time.Second {
|
||||
t.Fatalf("unexpected timeout: %s", intent.timeout)
|
||||
if got := normalizeRunWaitTimeout(0); got != defaultRunWaitTimeout {
|
||||
t.Fatalf("expected default timeout, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectRunControlIntentLatest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
intent, ok := detectRunControlIntent("latest run status")
|
||||
if !ok {
|
||||
t.Fatalf("expected latest run status intent")
|
||||
}
|
||||
if !intent.latest {
|
||||
t.Fatalf("expected latest=true")
|
||||
}
|
||||
if intent.runID != "" {
|
||||
t.Fatalf("expected empty run id")
|
||||
if got := normalizeRunWaitTimeout(time.Second); got != minRunWaitTimeout {
|
||||
t.Fatalf("expected min timeout %s, got %s", minRunWaitTimeout, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunWaitTimeout_MinClamp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := parseRunWaitTimeout("wait run-1-1 1 s")
|
||||
if got != minRunWaitTimeout {
|
||||
t.Fatalf("expected min timeout %s, got %s", minRunWaitTimeout, got)
|
||||
if got := normalizeRunWaitTimeout(maxRunWaitTimeout + time.Minute); got != maxRunWaitTimeout {
|
||||
t.Fatalf("expected max timeout %s, got %s", maxRunWaitTimeout, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunWaitTimeout_MinuteUnit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := parseRunWaitTimeout("等待 run-1-1 2 分钟")
|
||||
if got != 2*time.Minute {
|
||||
if got := normalizeRunWaitTimeout(2 * time.Minute); got != 2*time.Minute {
|
||||
t.Fatalf("expected 2m, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectRunControlIntentIgnoresNonControlText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, ok := detectRunControlIntent("帮我写一个README"); ok {
|
||||
t.Fatalf("did not expect run control intent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectRunControlIntentWithCustomLexicon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lex := runControlLexicon{
|
||||
latestKeywords: []string{"newest"},
|
||||
waitKeywords: []string{"block"},
|
||||
statusKeywords: []string{"health"},
|
||||
runMentionKeywords: []string{"job"},
|
||||
minuteUnits: map[string]struct{}{"mins": {}},
|
||||
}
|
||||
|
||||
intent, ok := detectRunControlIntentWithLexicon("block run-55-1 for 2 mins and show health", lex)
|
||||
if !ok {
|
||||
t.Fatalf("expected intent with custom lexicon")
|
||||
}
|
||||
if !intent.wait {
|
||||
t.Fatalf("expected wait=true")
|
||||
}
|
||||
if intent.timeout != 2*time.Minute {
|
||||
t.Fatalf("unexpected timeout: %s", intent.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestRunStateBySession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user