mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-19 02:37:29 +08:00
Add multi-agent config and registry runtime flow
This commit is contained in:
@@ -27,7 +27,70 @@ type Config struct {
|
||||
}
|
||||
|
||||
type AgentsConfig struct {
|
||||
Defaults AgentDefaults `json:"defaults"`
|
||||
Defaults AgentDefaults `json:"defaults"`
|
||||
Router AgentRouterConfig `json:"router,omitempty"`
|
||||
Communication AgentCommunicationConfig `json:"communication,omitempty"`
|
||||
Subagents map[string]SubagentConfig `json:"subagents,omitempty"`
|
||||
}
|
||||
|
||||
type AgentRouterConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
MainAgentID string `json:"main_agent_id,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
Rules []AgentRouteRule `json:"rules,omitempty"`
|
||||
AllowDirectAgentChat bool `json:"allow_direct_agent_chat,omitempty"`
|
||||
MaxHops int `json:"max_hops,omitempty"`
|
||||
DefaultTimeoutSec int `json:"default_timeout_sec,omitempty"`
|
||||
DefaultWaitReply bool `json:"default_wait_reply,omitempty"`
|
||||
StickyThreadOwner bool `json:"sticky_thread_owner,omitempty"`
|
||||
}
|
||||
|
||||
type AgentRouteRule struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
}
|
||||
|
||||
type AgentCommunicationConfig struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
PersistThreads bool `json:"persist_threads,omitempty"`
|
||||
PersistMessages bool `json:"persist_messages,omitempty"`
|
||||
MaxMessagesPerThread int `json:"max_messages_per_thread,omitempty"`
|
||||
DeadLetterQueue bool `json:"dead_letter_queue,omitempty"`
|
||||
DefaultMessageTTLSec int `json:"default_message_ttl_sec,omitempty"`
|
||||
}
|
||||
|
||||
type SubagentConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||
MemoryNamespace string `json:"memory_namespace,omitempty"`
|
||||
AcceptFrom []string `json:"accept_from,omitempty"`
|
||||
CanTalkTo []string `json:"can_talk_to,omitempty"`
|
||||
RequiresMainMediation bool `json:"requires_main_mediation,omitempty"`
|
||||
DefaultReplyTo string `json:"default_reply_to,omitempty"`
|
||||
Tools SubagentToolsConfig `json:"tools,omitempty"`
|
||||
Runtime SubagentRuntimeConfig `json:"runtime,omitempty"`
|
||||
}
|
||||
|
||||
type SubagentToolsConfig struct {
|
||||
Allowlist []string `json:"allowlist,omitempty"`
|
||||
Denylist []string `json:"denylist,omitempty"`
|
||||
MaxParallelCalls int `json:"max_parallel_calls,omitempty"`
|
||||
}
|
||||
|
||||
type SubagentRuntimeConfig struct {
|
||||
Proxy string `json:"proxy,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TimeoutSec int `json:"timeout_sec,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
RetryBackoffMs int `json:"retry_backoff_ms,omitempty"`
|
||||
MaxTaskChars int `json:"max_task_chars,omitempty"`
|
||||
MaxResultChars int `json:"max_result_chars,omitempty"`
|
||||
MaxParallelRuns int `json:"max_parallel_runs,omitempty"`
|
||||
}
|
||||
|
||||
type AgentDefaults struct {
|
||||
@@ -352,6 +415,26 @@ func DefaultConfig() *Config {
|
||||
},
|
||||
},
|
||||
},
|
||||
Router: AgentRouterConfig{
|
||||
Enabled: false,
|
||||
MainAgentID: "main",
|
||||
Strategy: "rules_first",
|
||||
Rules: []AgentRouteRule{},
|
||||
AllowDirectAgentChat: false,
|
||||
MaxHops: 6,
|
||||
DefaultTimeoutSec: 600,
|
||||
DefaultWaitReply: true,
|
||||
StickyThreadOwner: true,
|
||||
},
|
||||
Communication: AgentCommunicationConfig{
|
||||
Mode: "mediated",
|
||||
PersistThreads: true,
|
||||
PersistMessages: true,
|
||||
MaxMessagesPerThread: 100,
|
||||
DeadLetterQueue: true,
|
||||
DefaultMessageTTLSec: 86400,
|
||||
},
|
||||
Subagents: map[string]SubagentConfig{},
|
||||
},
|
||||
Channels: ChannelsConfig{
|
||||
InboundMessageIDDedupeTTLSeconds: 600,
|
||||
|
||||
@@ -116,6 +116,9 @@ func Validate(cfg *Config) []error {
|
||||
errs = append(errs, fmt.Errorf("context_compaction.mode=responses_compact requires active proxy %q with supports_responses_compact=true", active))
|
||||
}
|
||||
}
|
||||
errs = append(errs, validateAgentRouter(cfg)...)
|
||||
errs = append(errs, validateAgentCommunication(cfg)...)
|
||||
errs = append(errs, validateSubagents(cfg)...)
|
||||
|
||||
if cfg.Gateway.Port <= 0 || cfg.Gateway.Port > 65535 {
|
||||
errs = append(errs, fmt.Errorf("gateway.port must be in 1..65535"))
|
||||
@@ -213,6 +216,170 @@ func Validate(cfg *Config) []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateAgentRouter(cfg *Config) []error {
|
||||
router := cfg.Agents.Router
|
||||
var errs []error
|
||||
if strings.TrimSpace(router.Strategy) != "" {
|
||||
switch strings.TrimSpace(router.Strategy) {
|
||||
case "rules_first", "round_robin", "manual":
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("agents.router.strategy must be one of: rules_first, round_robin, manual"))
|
||||
}
|
||||
}
|
||||
if router.MaxHops < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.router.max_hops must be >= 0"))
|
||||
}
|
||||
if router.DefaultTimeoutSec < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.router.default_timeout_sec must be >= 0"))
|
||||
}
|
||||
if router.Enabled && strings.TrimSpace(router.MainAgentID) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.router.main_agent_id is required when agents.router.enabled=true"))
|
||||
}
|
||||
for i, rule := range router.Rules {
|
||||
agentID := strings.TrimSpace(rule.AgentID)
|
||||
if agentID == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.router.rules[%d].agent_id is required", i))
|
||||
continue
|
||||
}
|
||||
if _, ok := cfg.Agents.Subagents[agentID]; !ok {
|
||||
errs = append(errs, fmt.Errorf("agents.router.rules[%d].agent_id %q not found in agents.subagents", i, agentID))
|
||||
}
|
||||
if len(rule.Keywords) == 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.router.rules[%d].keywords must not be empty", i))
|
||||
}
|
||||
for _, kw := range rule.Keywords {
|
||||
if strings.TrimSpace(kw) == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.router.rules[%d].keywords must not contain empty values", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateAgentCommunication(cfg *Config) []error {
|
||||
comm := cfg.Agents.Communication
|
||||
var errs []error
|
||||
if strings.TrimSpace(comm.Mode) != "" {
|
||||
switch strings.TrimSpace(comm.Mode) {
|
||||
case "mediated", "direct":
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("agents.communication.mode must be one of: mediated, direct"))
|
||||
}
|
||||
}
|
||||
if comm.MaxMessagesPerThread < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.communication.max_messages_per_thread must be >= 0"))
|
||||
}
|
||||
if comm.DefaultMessageTTLSec < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.communication.default_message_ttl_sec must be >= 0"))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateSubagents(cfg *Config) []error {
|
||||
var errs []error
|
||||
if len(cfg.Agents.Subagents) == 0 {
|
||||
return errs
|
||||
}
|
||||
mainID := strings.TrimSpace(cfg.Agents.Router.MainAgentID)
|
||||
if cfg.Agents.Router.Enabled && mainID != "" {
|
||||
if _, ok := cfg.Agents.Subagents[mainID]; !ok {
|
||||
errs = append(errs, fmt.Errorf("agents.router.main_agent_id %q not found in agents.subagents", mainID))
|
||||
}
|
||||
}
|
||||
for agentID, raw := range cfg.Agents.Subagents {
|
||||
id := strings.TrimSpace(agentID)
|
||||
if id == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents contains an empty agent id"))
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(raw.Type) != "" {
|
||||
switch strings.TrimSpace(raw.Type) {
|
||||
case "router", "worker", "reviewer", "observer":
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.type must be one of: router, worker, reviewer, observer", id))
|
||||
}
|
||||
}
|
||||
if raw.Runtime.TimeoutSec < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.timeout_sec must be >= 0", id))
|
||||
}
|
||||
if raw.Runtime.MaxRetries < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.max_retries must be >= 0", id))
|
||||
}
|
||||
if raw.Runtime.RetryBackoffMs < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.retry_backoff_ms must be >= 0", id))
|
||||
}
|
||||
if raw.Runtime.MaxTaskChars < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.max_task_chars must be >= 0", id))
|
||||
}
|
||||
if raw.Runtime.MaxResultChars < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.max_result_chars must be >= 0", id))
|
||||
}
|
||||
if raw.Runtime.MaxParallelRuns < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.max_parallel_runs must be >= 0", id))
|
||||
}
|
||||
if raw.Tools.MaxParallelCalls < 0 {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.tools.max_parallel_calls must be >= 0", id))
|
||||
}
|
||||
if proxy := strings.TrimSpace(raw.Runtime.Proxy); proxy != "" && !providerExists(cfg, proxy) {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.runtime.proxy %q not found in providers", id, proxy))
|
||||
}
|
||||
for _, sender := range raw.AcceptFrom {
|
||||
sender = strings.TrimSpace(sender)
|
||||
if sender == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.accept_from must not contain empty values", id))
|
||||
continue
|
||||
}
|
||||
if sender != "user" && sender != id {
|
||||
if _, ok := cfg.Agents.Subagents[sender]; !ok {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.accept_from references unknown agent %q", id, sender))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, target := range raw.CanTalkTo {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.can_talk_to must not contain empty values", id))
|
||||
continue
|
||||
}
|
||||
if target != "user" {
|
||||
if _, ok := cfg.Agents.Subagents[target]; !ok {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.can_talk_to references unknown agent %q", id, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw.RequiresMainMediation && mainID != "" && id == mainID {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.requires_main_mediation must be false for main agent", id))
|
||||
}
|
||||
}
|
||||
for agentID, raw := range cfg.Agents.Subagents {
|
||||
id := strings.TrimSpace(agentID)
|
||||
for _, target := range raw.CanTalkTo {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" || target == "user" {
|
||||
continue
|
||||
}
|
||||
peer, ok := cfg.Agents.Subagents[target]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !containsString(raw.AcceptFrom, target) && !containsString(peer.AcceptFrom, id) {
|
||||
errs = append(errs, fmt.Errorf("agents.subagents.%s.can_talk_to %q is not reciprocated by accept_from", id, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func containsString(items []string, target string) bool {
|
||||
target = strings.TrimSpace(target)
|
||||
for _, item := range items {
|
||||
if strings.TrimSpace(item) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateProviderConfig(path string, p ProviderConfig) []error {
|
||||
var errs []error
|
||||
if p.APIBase == "" {
|
||||
|
||||
44
pkg/config/validate_test.go
Normal file
44
pkg/config/validate_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateSubagentsAllowsKnownPeers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Agents.Router.Enabled = true
|
||||
cfg.Agents.Router.MainAgentID = "main"
|
||||
cfg.Agents.Subagents["main"] = SubagentConfig{
|
||||
Enabled: true,
|
||||
Type: "router",
|
||||
AcceptFrom: []string{"user", "coder"},
|
||||
CanTalkTo: []string{"coder"},
|
||||
}
|
||||
cfg.Agents.Subagents["coder"] = SubagentConfig{
|
||||
Enabled: true,
|
||||
Type: "worker",
|
||||
AcceptFrom: []string{"main"},
|
||||
CanTalkTo: []string{"main"},
|
||||
Runtime: SubagentRuntimeConfig{
|
||||
Proxy: "proxy",
|
||||
},
|
||||
}
|
||||
|
||||
if errs := Validate(cfg); len(errs) != 0 {
|
||||
t.Fatalf("expected config to be valid, got %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSubagentsRejectsUnknownPeer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Agents.Subagents["coder"] = SubagentConfig{
|
||||
Enabled: true,
|
||||
AcceptFrom: []string{"main"},
|
||||
}
|
||||
|
||||
if errs := Validate(cfg); len(errs) == 0 {
|
||||
t.Fatalf("expected validation errors")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user