mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-17 01:47:30 +08:00
fix loop
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -35,6 +38,31 @@ type AgentDefaults struct {
|
||||
Temperature float64 `json:"temperature" env:"CLAWGO_AGENTS_DEFAULTS_TEMPERATURE"`
|
||||
MaxToolIterations int `json:"max_tool_iterations" env:"CLAWGO_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
|
||||
ContextCompaction ContextCompactionConfig `json:"context_compaction"`
|
||||
RuntimeControl RuntimeControlConfig `json:"runtime_control"`
|
||||
}
|
||||
|
||||
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"`
|
||||
AutonomyMaxRoundsWithoutUser int `json:"autonomy_max_rounds_without_user" env:"CLAWGO_AUTONOMY_MAX_ROUNDS_WITHOUT_USER"`
|
||||
AutonomyMaxPendingDurationSec int `json:"autonomy_max_pending_duration_sec" env:"CLAWGO_AUTONOMY_MAX_PENDING_DURATION_SEC"`
|
||||
AutonomyMaxConsecutiveStalls int `json:"autonomy_max_consecutive_stalls" env:"CLAWGO_AUTONOMY_MAX_STALLS"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type ContextCompactionConfig struct {
|
||||
@@ -246,6 +274,29 @@ func DefaultConfig() *Config {
|
||||
MaxSummaryChars: 6000,
|
||||
MaxTranscriptChars: 20000,
|
||||
},
|
||||
RuntimeControl: RuntimeControlConfig{
|
||||
IntentHighConfidence: 0.75,
|
||||
IntentConfirmMinConfidence: 0.45,
|
||||
IntentMaxInputChars: 1200,
|
||||
ConfirmTTLSeconds: 300,
|
||||
ConfirmMaxClarificationTurns: 2,
|
||||
AutonomyTickIntervalSec: 20,
|
||||
AutonomyMinRunIntervalSec: 20,
|
||||
AutonomyIdleThresholdSec: 20,
|
||||
AutonomyMaxRoundsWithoutUser: 120,
|
||||
AutonomyMaxPendingDurationSec: 180,
|
||||
AutonomyMaxConsecutiveStalls: 3,
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
Channels: ChannelsConfig{
|
||||
@@ -382,7 +433,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
if err := unmarshalConfigStrict(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -393,6 +444,22 @@ func LoadConfig(path string) (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func unmarshalConfigStrict(data []byte, cfg *Config) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var extra json.RawMessage
|
||||
if err := dec.Decode(&extra); err != io.EOF {
|
||||
if err == nil {
|
||||
return fmt.Errorf("invalid config: trailing JSON content")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveConfig(path string, cfg *Config) error {
|
||||
cfg.mu.RLock()
|
||||
defer cfg.mu.RUnlock()
|
||||
|
||||
95
pkg/config/config_test.go
Normal file
95
pkg/config/config_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfigRejectsUnknownField(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"runtime_control": {
|
||||
"intent_high_confidence": 0.8,
|
||||
"unknown_field": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadConfig(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected unknown field error")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "unknown field") {
|
||||
t.Fatalf("expected unknown field error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigRejectsTrailingJSONContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := `{"agents":{"defaults":{"runtime_control":{"intent_high_confidence":0.8}}}}{"extra":true}`
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadConfig(cfgPath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected trailing json content error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "trailing JSON content") {
|
||||
t.Fatalf("expected trailing JSON content error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigAllowsKnownRuntimeControlFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := `{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load config: %v", err)
|
||||
}
|
||||
if got := cfg.Agents.Defaults.RuntimeControl.IntentHighConfidence; got != 0.88 {
|
||||
t.Fatalf("intent_high_confidence mismatch: got %.2f", got)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,58 @@ func Validate(cfg *Config) []error {
|
||||
if cfg.Agents.Defaults.MaxToolIterations <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.max_tool_iterations must be > 0"))
|
||||
}
|
||||
rc := cfg.Agents.Defaults.RuntimeControl
|
||||
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"))
|
||||
}
|
||||
if rc.AutonomyMinRunIntervalSec < 5 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autonomy_min_run_interval_sec must be >= 5"))
|
||||
}
|
||||
if rc.AutonomyIdleThresholdSec < 5 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autonomy_idle_threshold_sec must be >= 5"))
|
||||
}
|
||||
if rc.AutonomyMaxRoundsWithoutUser <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autonomy_max_rounds_without_user must be > 0"))
|
||||
}
|
||||
if rc.AutonomyMaxPendingDurationSec < 10 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autonomy_max_pending_duration_sec must be >= 10"))
|
||||
}
|
||||
if rc.AutonomyMaxConsecutiveStalls <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autonomy_max_consecutive_stalls must be > 0"))
|
||||
}
|
||||
if rc.AutoLearnMaxRoundsWithoutUser <= 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.autolearn_max_rounds_without_user must be > 0"))
|
||||
}
|
||||
if rc.RunStateTTLSeconds < 60 {
|
||||
errs = append(errs, fmt.Errorf("agents.defaults.runtime_control.run_state_ttl_seconds must be >= 60"))
|
||||
}
|
||||
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"))
|
||||
}
|
||||
if cfg.Agents.Defaults.ContextCompaction.Enabled {
|
||||
cc := cfg.Agents.Defaults.ContextCompaction
|
||||
if cc.Mode != "" {
|
||||
@@ -199,3 +251,16 @@ func providerConfigByName(cfg *Config, name string) (ProviderConfig, bool) {
|
||||
pc, ok := cfg.Providers.Proxies[name]
|
||||
return pc, ok
|
||||
}
|
||||
|
||||
func validateNonEmptyStringList(path string, values []string) []error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
for i, value := range values {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
errs = append(errs, fmt.Errorf("%s[%d] must not be empty", path, i))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user