feat(teams): implement heterogeneous agent model routing
- Added 'model' property to 'team' and 'spawn_sub_agent' JSON schemas. - Modified 'buildWorkerConfig' to override 'baseConfig.Model' when a specific LLM model is requested by the coordinator. - Allows teams to dynamically mix and match specialized vision, coding, and logical models within the same execution loop.
This commit is contained in:
parent
e7de26ee1b
commit
429ce66a7d
2 changed files with 50 additions and 15 deletions
|
|
@ -43,6 +43,10 @@ func (t *SpawnSubAgentTool) Parameters() map[string]any {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The system prompt/role assignment for the sub-agent (e.g., 'You are an expert code reviewer').",
|
"description": "The system prompt/role assignment for the sub-agent (e.g., 'You are an expert code reviewer').",
|
||||||
},
|
},
|
||||||
|
"model": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional specific LLM model ID to route this task to (e.g., 'gpt-4o' for vision, 'claude-3-5-sonnet' for logic). If omitted, inherits the parent's model.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"task", "role"},
|
"required": []string{"task", "role"},
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +87,11 @@ func (t *SpawnSubAgentTool) Execute(ctx context.Context, args map[string]any) *T
|
||||||
// 2. Base Configuration (Timeout & LLM constraints)
|
// 2. Base Configuration (Timeout & LLM constraints)
|
||||||
config := t.manager.BuildBaseWorkerConfig(ctx)
|
config := t.manager.BuildBaseWorkerConfig(ctx)
|
||||||
|
|
||||||
|
// 2.1 Model Override (Heterogeneous Agents)
|
||||||
|
if modelParam, ok := args["model"].(string); ok && strings.TrimSpace(modelParam) != "" {
|
||||||
|
config.Model = strings.TrimSpace(modelParam)
|
||||||
|
}
|
||||||
|
|
||||||
// Note: For MVP, we pass the current ToolRegistry unmodified.
|
// Note: For MVP, we pass the current ToolRegistry unmodified.
|
||||||
// To enforce strict sandboxing later, we can construct a new ToolRegistry here based on args['allowed_tools'].
|
// To enforce strict sandboxing later, we can construct a new ToolRegistry here based on args['allowed_tools'].
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ type TeamMember struct {
|
||||||
ID string
|
ID string
|
||||||
Role string
|
Role string
|
||||||
Task string
|
Task string
|
||||||
|
Model string // Heterogeneous Agents: Optional specific model for this task
|
||||||
DependsOn []string // List of member IDs this member depends on
|
DependsOn []string // List of member IDs this member depends on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +72,10 @@ func (t *TeamTool) Parameters() map[string]any {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The specific task this member needs to accomplish.",
|
"description": "The specific task this member needs to accomplish.",
|
||||||
},
|
},
|
||||||
|
"model": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional specific LLM model ID to route this task to (e.g., 'gpt-4o' for vision, 'claude-3-5-sonnet' for logic). If omitted, inherits the parent's model.",
|
||||||
|
},
|
||||||
"depends_on": map[string]any{
|
"depends_on": map[string]any{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "List of 'id' strings this member depends on. Only applicable for 'dag' strategy.",
|
"description": "List of 'id' strings this member depends on. Only applicable for 'dag' strategy.",
|
||||||
|
|
@ -132,6 +137,9 @@ func (t *TeamTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
id = fmt.Sprintf("member_%d", i)
|
id = fmt.Sprintf("member_%d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modelStr, _ := mMap["model"].(string)
|
||||||
|
modelStr = strings.TrimSpace(modelStr)
|
||||||
|
|
||||||
var dependsOn []string
|
var dependsOn []string
|
||||||
if depRaw, dOk := mMap["depends_on"].([]any); dOk {
|
if depRaw, dOk := mMap["depends_on"].([]any); dOk {
|
||||||
for _, d := range depRaw {
|
for _, d := range depRaw {
|
||||||
|
|
@ -145,6 +153,7 @@ func (t *TeamTool) Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
ID: id,
|
ID: id,
|
||||||
Role: role,
|
Role: role,
|
||||||
Task: task,
|
Task: task,
|
||||||
|
Model: modelStr,
|
||||||
DependsOn: dependsOn,
|
DependsOn: dependsOn,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +208,19 @@ func upgradeRegistryForConcurrency(original *ToolRegistry) *ToolRegistry {
|
||||||
return upgraded
|
return upgraded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) executeSequential(ctx context.Context, config ToolLoopConfig, members []TeamMember) *ToolResult {
|
// buildWorkerConfig creates a ToolLoopConfig for a specific team member,
|
||||||
|
// potentially overriding the model based on the member's definition.
|
||||||
|
func buildWorkerConfig(baseConfig ToolLoopConfig, registry *ToolRegistry, m TeamMember) ToolLoopConfig {
|
||||||
|
cfg := baseConfig
|
||||||
|
cfg.Tools = registry
|
||||||
|
// Heterogeneous Agents: Override model if this team member requested a specific one
|
||||||
|
if m.Model != "" {
|
||||||
|
cfg.Model = m.Model
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TeamTool) executeSequential(ctx context.Context, baseConfig ToolLoopConfig, members []TeamMember) *ToolResult {
|
||||||
var finalOutput strings.Builder
|
var finalOutput strings.Builder
|
||||||
finalOutput.WriteString("Team Execution Summary (Sequential):\n\n")
|
finalOutput.WriteString("Team Execution Summary (Sequential):\n\n")
|
||||||
|
|
||||||
|
|
@ -217,7 +238,8 @@ func (t *TeamTool) executeSequential(ctx context.Context, config ToolLoopConfig,
|
||||||
{Role: "user", Content: actualTask},
|
{Role: "user", Content: actualTask},
|
||||||
}
|
}
|
||||||
|
|
||||||
loopResult, err := RunToolLoop(ctx, config, messages, t.originChannel, t.originChatID)
|
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, m)
|
||||||
|
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Phase %d (Role: %s) failed: %v", i+1, m.Role, err)
|
errStr := fmt.Sprintf("Phase %d (Role: %s) failed: %v", i+1, m.Role, err)
|
||||||
finalOutput.WriteString(errStr + "\n")
|
finalOutput.WriteString(errStr + "\n")
|
||||||
|
|
@ -235,7 +257,7 @@ func (t *TeamTool) executeSequential(ctx context.Context, config ToolLoopConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) executeParallel(ctx context.Context, config ToolLoopConfig, members []TeamMember) *ToolResult {
|
func (t *TeamTool) executeParallel(ctx context.Context, baseConfig ToolLoopConfig, members []TeamMember) *ToolResult {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
type workResult struct {
|
type workResult struct {
|
||||||
index int
|
index int
|
||||||
|
|
@ -248,22 +270,23 @@ func (t *TeamTool) executeParallel(ctx context.Context, config ToolLoopConfig, m
|
||||||
|
|
||||||
for i, m := range members {
|
for i, m := range members {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(index int, role, task string) {
|
go func(index int, member TeamMember) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
messages := []providers.Message{
|
messages := []providers.Message{
|
||||||
{Role: "system", Content: role},
|
{Role: "system", Content: member.Role},
|
||||||
{Role: "user", Content: task},
|
{Role: "user", Content: member.Task},
|
||||||
}
|
}
|
||||||
|
|
||||||
loopResult, err := RunToolLoop(ctx, config, messages, t.originChannel, t.originChatID)
|
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, member)
|
||||||
|
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resultsChan <- workResult{index: index, role: role, err: err}
|
resultsChan <- workResult{index: index, role: member.Role, err: err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultsChan <- workResult{index: index, role: role, res: loopResult.Content}
|
resultsChan <- workResult{index: index, role: member.Role, res: loopResult.Content}
|
||||||
}(i, m.Role, m.Task)
|
}(i, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all goroutines to finish
|
// Wait for all goroutines to finish
|
||||||
|
|
@ -299,7 +322,7 @@ func (t *TeamTool) executeParallel(ctx context.Context, config ToolLoopConfig, m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, config ToolLoopConfig, members []TeamMember) *ToolResult {
|
func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, baseConfig ToolLoopConfig, members []TeamMember) *ToolResult {
|
||||||
if len(members) != 2 {
|
if len(members) != 2 {
|
||||||
return ErrorResult("The evaluator_optimizer strategy requires exactly two members: [0] Worker, [1] Evaluator.")
|
return ErrorResult("The evaluator_optimizer strategy requires exactly two members: [0] Worker, [1] Evaluator.")
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +344,8 @@ func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, config ToolLoo
|
||||||
finalOutput.WriteString(fmt.Sprintf("## Attempt %d\n", attempt))
|
finalOutput.WriteString(fmt.Sprintf("## Attempt %d\n", attempt))
|
||||||
|
|
||||||
// 2. Trigger Worker (resumes from its exact previous state!)
|
// 2. Trigger Worker (resumes from its exact previous state!)
|
||||||
workerResult, err := RunToolLoop(ctx, config, workerMessages, t.originChannel, t.originChatID)
|
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, worker)
|
||||||
|
workerResult, err := RunToolLoop(ctx, workerConfig, workerMessages, t.originChannel, t.originChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Worker failed on attempt %d: %v", attempt, err)
|
errStr := fmt.Sprintf("Worker failed on attempt %d: %v", attempt, err)
|
||||||
finalOutput.WriteString(errStr + "\n")
|
finalOutput.WriteString(errStr + "\n")
|
||||||
|
|
@ -341,7 +365,8 @@ func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, config ToolLoo
|
||||||
{Role: "user", Content: evalContext},
|
{Role: "user", Content: evalContext},
|
||||||
}
|
}
|
||||||
|
|
||||||
evalResult, err := RunToolLoop(ctx, config, evalMessages, t.originChannel, t.originChatID)
|
evalConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, evaluator)
|
||||||
|
evalResult, err := RunToolLoop(ctx, evalConfig, evalMessages, t.originChannel, t.originChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errStr := fmt.Sprintf("Evaluator failed on attempt %d: %v", attempt, err)
|
errStr := fmt.Sprintf("Evaluator failed on attempt %d: %v", attempt, err)
|
||||||
finalOutput.WriteString(errStr + "\n")
|
finalOutput.WriteString(errStr + "\n")
|
||||||
|
|
@ -376,7 +401,7 @@ func (t *TeamTool) executeEvaluatorOptimizer(ctx context.Context, config ToolLoo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TeamTool) executeDAG(ctx context.Context, config ToolLoopConfig, members []TeamMember) *ToolResult {
|
func (t *TeamTool) executeDAG(ctx context.Context, baseConfig ToolLoopConfig, members []TeamMember) *ToolResult {
|
||||||
// 1. Build and VALIDATE dependency graph
|
// 1. Build and VALIDATE dependency graph
|
||||||
memberMap := make(map[string]TeamMember)
|
memberMap := make(map[string]TeamMember)
|
||||||
inDegree := make(map[string]int)
|
inDegree := make(map[string]int)
|
||||||
|
|
@ -489,7 +514,8 @@ func (t *TeamTool) executeDAG(ctx context.Context, config ToolLoopConfig, member
|
||||||
{Role: "user", Content: actualTask},
|
{Role: "user", Content: actualTask},
|
||||||
}
|
}
|
||||||
|
|
||||||
loopResult, err := RunToolLoop(ctx, config, messages, t.originChannel, t.originChatID)
|
workerConfig := buildWorkerConfig(baseConfig, baseConfig.Tools, m)
|
||||||
|
loopResult, err := RunToolLoop(ctx, workerConfig, messages, t.originChannel, t.originChatID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
masterErrMu.Lock()
|
masterErrMu.Lock()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue