mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 09:13:23 +08:00
fix loop
This commit is contained in:
@@ -166,10 +166,7 @@ clawgo channel test --channel telegram --to <chat_id> -m "ping"
|
||||
"defaults": {
|
||||
"runtime_control": {
|
||||
"intent_high_confidence": 0.75,
|
||||
"intent_confirm_min_confidence": 0.45,
|
||||
"intent_max_input_chars": 1200,
|
||||
"confirm_ttl_seconds": 300,
|
||||
"confirm_max_clarification_turns": 2,
|
||||
"autonomy_tick_interval_sec": 20,
|
||||
"autonomy_min_run_interval_sec": 20,
|
||||
"autonomy_idle_threshold_sec": 20,
|
||||
@@ -179,11 +176,6 @@ clawgo channel test --channel telegram --to <chat_id> -m "ping"
|
||||
"autolearn_max_rounds_without_user": 200,
|
||||
"run_state_ttl_seconds": 1800,
|
||||
"run_state_max": 500,
|
||||
"run_control_latest_keywords": ["latest", "last run", "recent run", "最新", "最近", "上一次", "上个"],
|
||||
"run_control_wait_keywords": ["wait", "等待", "等到", "阻塞"],
|
||||
"run_control_status_keywords": ["status", "状态", "进度", "running", "运行"],
|
||||
"run_control_run_mention_keywords": ["run", "任务"],
|
||||
"run_control_minute_units": ["分钟", "min", "mins", "minute", "minutes", "m"],
|
||||
"tool_parallel_safe_names": ["read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"],
|
||||
"tool_max_parallel_calls": 2
|
||||
}
|
||||
|
||||
@@ -166,10 +166,7 @@ Runtime-control config example (intent thresholds / autonomy guards / run-state
|
||||
"defaults": {
|
||||
"runtime_control": {
|
||||
"intent_high_confidence": 0.75,
|
||||
"intent_confirm_min_confidence": 0.45,
|
||||
"intent_max_input_chars": 1200,
|
||||
"confirm_ttl_seconds": 300,
|
||||
"confirm_max_clarification_turns": 2,
|
||||
"autonomy_tick_interval_sec": 20,
|
||||
"autonomy_min_run_interval_sec": 20,
|
||||
"autonomy_idle_threshold_sec": 20,
|
||||
@@ -179,11 +176,6 @@ Runtime-control config example (intent thresholds / autonomy guards / run-state
|
||||
"autolearn_max_rounds_without_user": 200,
|
||||
"run_state_ttl_seconds": 1800,
|
||||
"run_state_max": 500,
|
||||
"run_control_latest_keywords": ["latest", "last run", "recent run", "最新", "最近", "上一次", "上个"],
|
||||
"run_control_wait_keywords": ["wait", "等待", "等到", "阻塞"],
|
||||
"run_control_status_keywords": ["status", "状态", "进度", "running", "运行"],
|
||||
"run_control_run_mention_keywords": ["run", "任务"],
|
||||
"run_control_minute_units": ["分钟", "min", "mins", "minute", "minutes", "m"],
|
||||
"tool_parallel_safe_names": ["read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"],
|
||||
"tool_max_parallel_calls": 2
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
},
|
||||
"runtime_control": {
|
||||
"intent_high_confidence": 0.75,
|
||||
"intent_confirm_min_confidence": 0.45,
|
||||
"intent_max_input_chars": 1200,
|
||||
"confirm_ttl_seconds": 300,
|
||||
"confirm_max_clarification_turns": 2,
|
||||
"autonomy_tick_interval_sec": 20,
|
||||
"autonomy_min_run_interval_sec": 20,
|
||||
"autonomy_idle_threshold_sec": 20,
|
||||
@@ -30,11 +27,6 @@
|
||||
"autolearn_max_rounds_without_user": 200,
|
||||
"run_state_ttl_seconds": 1800,
|
||||
"run_state_max": 500,
|
||||
"run_control_latest_keywords": ["latest", "last run", "recent run", "最新", "最近", "上一次", "上个"],
|
||||
"run_control_wait_keywords": ["wait", "等待", "等到", "阻塞"],
|
||||
"run_control_status_keywords": ["status", "状态", "进度", "running", "运行"],
|
||||
"run_control_run_mention_keywords": ["run", "任务"],
|
||||
"run_control_minute_units": ["分钟", "min", "mins", "minute", "minutes", "m"],
|
||||
"tool_parallel_safe_names": ["read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"],
|
||||
"tool_max_parallel_calls": 2
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -43,10 +43,7 @@ type AgentDefaults struct {
|
||||
|
||||
type RuntimeControlConfig struct {
|
||||
IntentHighConfidence float64 `json:"intent_high_confidence" env:"CLAWGO_INTENT_HIGH_CONFIDENCE"`
|
||||
IntentConfirmMinConfidence float64 `json:"intent_confirm_min_confidence" env:"CLAWGO_INTENT_CONFIRM_MIN_CONFIDENCE"`
|
||||
IntentMaxInputChars int `json:"intent_max_input_chars" env:"CLAWGO_INTENT_MAX_INPUT_CHARS"`
|
||||
ConfirmTTLSeconds int `json:"confirm_ttl_seconds" env:"CLAWGO_CONFIRM_TTL_SECONDS"`
|
||||
ConfirmMaxClarificationTurns int `json:"confirm_max_clarification_turns" env:"CLAWGO_CONFIRM_MAX_CLARIFY_TURNS"`
|
||||
AutonomyTickIntervalSec int `json:"autonomy_tick_interval_sec" env:"CLAWGO_AUTONOMY_TICK_INTERVAL_SEC"`
|
||||
AutonomyMinRunIntervalSec int `json:"autonomy_min_run_interval_sec" env:"CLAWGO_AUTONOMY_MIN_RUN_INTERVAL_SEC"`
|
||||
AutonomyIdleThresholdSec int `json:"autonomy_idle_threshold_sec" env:"CLAWGO_AUTONOMY_IDLE_THRESHOLD_SEC"`
|
||||
@@ -56,11 +53,6 @@ type RuntimeControlConfig struct {
|
||||
AutoLearnMaxRoundsWithoutUser int `json:"autolearn_max_rounds_without_user" env:"CLAWGO_AUTOLEARN_MAX_ROUNDS_WITHOUT_USER"`
|
||||
RunStateTTLSeconds int `json:"run_state_ttl_seconds" env:"CLAWGO_RUN_STATE_TTL_SECONDS"`
|
||||
RunStateMax int `json:"run_state_max" env:"CLAWGO_RUN_STATE_MAX"`
|
||||
RunControlLatestKeywords []string `json:"run_control_latest_keywords"`
|
||||
RunControlWaitKeywords []string `json:"run_control_wait_keywords"`
|
||||
RunControlStatusKeywords []string `json:"run_control_status_keywords"`
|
||||
RunControlRunMentionKeywords []string `json:"run_control_run_mention_keywords"`
|
||||
RunControlMinuteUnits []string `json:"run_control_minute_units"`
|
||||
ToolParallelSafeNames []string `json:"tool_parallel_safe_names"`
|
||||
ToolMaxParallelCalls int `json:"tool_max_parallel_calls"`
|
||||
}
|
||||
@@ -262,10 +254,7 @@ func DefaultConfig() *Config {
|
||||
},
|
||||
RuntimeControl: RuntimeControlConfig{
|
||||
IntentHighConfidence: 0.75,
|
||||
IntentConfirmMinConfidence: 0.45,
|
||||
IntentMaxInputChars: 1200,
|
||||
ConfirmTTLSeconds: 300,
|
||||
ConfirmMaxClarificationTurns: 2,
|
||||
AutonomyTickIntervalSec: 20,
|
||||
AutonomyMinRunIntervalSec: 20,
|
||||
AutonomyIdleThresholdSec: 20,
|
||||
@@ -275,11 +264,6 @@ func DefaultConfig() *Config {
|
||||
AutoLearnMaxRoundsWithoutUser: 200,
|
||||
RunStateTTLSeconds: 1800,
|
||||
RunStateMax: 500,
|
||||
RunControlLatestKeywords: []string{"latest", "last run", "recent run", "最新", "最近", "上一次", "上个"},
|
||||
RunControlWaitKeywords: []string{"wait", "等待", "等到", "阻塞"},
|
||||
RunControlStatusKeywords: []string{"status", "状态", "进度", "running", "运行"},
|
||||
RunControlRunMentionKeywords: []string{"run", "任务"},
|
||||
RunControlMinuteUnits: []string{"分钟", "min", "mins", "minute", "minutes", "m"},
|
||||
ToolParallelSafeNames: []string{"read_file", "list_files", "find_files", "grep_files", "memory_search", "web_search", "repo_map", "system_info"},
|
||||
ToolMaxParallelCalls: 2,
|
||||
},
|
||||
|
||||
@@ -65,7 +65,6 @@ func TestLoadConfigAllowsKnownRuntimeControlFields(t *testing.T) {
|
||||
"runtime_control": {
|
||||
"intent_high_confidence": 0.88,
|
||||
"run_state_max": 321,
|
||||
"run_control_wait_keywords": ["wait", "block"],
|
||||
"tool_parallel_safe_names": ["read_file", "memory_search"],
|
||||
"tool_max_parallel_calls": 3
|
||||
}
|
||||
@@ -86,9 +85,6 @@ func TestLoadConfigAllowsKnownRuntimeControlFields(t *testing.T) {
|
||||
if got := cfg.Agents.Defaults.RuntimeControl.RunStateMax; got != 321 {
|
||||
t.Fatalf("run_state_max mismatch: got %d", got)
|
||||
}
|
||||
if got := len(cfg.Agents.Defaults.RuntimeControl.RunControlWaitKeywords); got != 2 {
|
||||
t.Fatalf("run_control_wait_keywords mismatch: got %d", got)
|
||||
}
|
||||
if got := cfg.Agents.Defaults.RuntimeControl.ToolMaxParallelCalls; got != 3 {
|
||||
t.Fatalf("tool_max_parallel_calls mismatch: got %d", got)
|
||||
}
|
||||
|
||||
@@ -21,18 +21,9 @@ func Validate(cfg *Config) []error {
|
||||
if rc.IntentHighConfidence <= 0 || rc.IntentHighConfidence > 1 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.intent_high_confidence must be in (0,1]"))
|
||||
}
|
||||
if rc.IntentConfirmMinConfidence < 0 || rc.IntentConfirmMinConfidence >= rc.IntentHighConfidence {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.intent_confirm_min_confidence must be >= 0 and < intent_high_confidence"))
|
||||
}
|
||||
if rc.IntentMaxInputChars < 200 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.intent_max_input_chars must be >= 200"))
|
||||
}
|
||||
if rc.ConfirmTTLSeconds <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.confirm_ttl_seconds must be > 0"))
|
||||
}
|
||||
if rc.ConfirmMaxClarificationTurns < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.confirm_max_clarification_turns must be >= 0"))
|
||||
}
|
||||
if rc.AutonomyTickIntervalSec < 5 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autonomy_tick_interval_sec must be >= 5"))
|
||||
}
|
||||
@@ -60,11 +51,6 @@ func Validate(cfg *Config) []error {
|
||||
if rc.RunStateMax <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.run_state_max must be > 0"))
|
||||
}
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.run_control_latest_keywords", rc.RunControlLatestKeywords)...)
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.run_control_wait_keywords", rc.RunControlWaitKeywords)...)
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.run_control_status_keywords", rc.RunControlStatusKeywords)...)
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.run_control_run_mention_keywords", rc.RunControlRunMentionKeywords)...)
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.run_control_minute_units", rc.RunControlMinuteUnits)...)
|
||||
errs = append(errs, validateNonEmptyStringList("agents.defaults.runtime_control.tool_parallel_safe_names", rc.ToolParallelSafeNames)...)
|
||||
if rc.ToolMaxParallelCalls <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.tool_max_parallel_calls must be > 0"))
|
||||
|
||||
Reference in New Issue
Block a user