yao/agent/caller/orchestrator.go
Max 3a9f32af12 Enhance Agent Context with JSAPI Initialization and Message Handling
- Added initialization for the Agent JSAPI factory to support ctx.agent.* methods, improving agent interaction capabilities.
- Introduced a new agent object in the JSAPI context for calling other agents, enhancing modularity.
- Implemented an OnMessage callback in the context options to handle messages sent via ctx.Send(), allowing for more flexible message processing.
2026-01-25 19:42:56 +08:00

286 lines
6.6 KiB
Go

package caller
import (
"sync"
agentContext "github.com/yaoapp/yao/agent/context"
)
// Orchestrator handles parallel agent calls with different concurrency patterns
// Modeled after JavaScript Promise patterns (all, any, race)
type Orchestrator struct {
ctx *agentContext.Context
}
// NewOrchestrator creates a new Orchestrator for parallel agent calls
func NewOrchestrator(ctx *agentContext.Context) *Orchestrator {
return &Orchestrator{ctx: ctx}
}
// callResult is used internally to pass results through channels
type callResult struct {
idx int
result *Result
}
// All executes all agent calls and waits for all to complete (like Promise.all)
// Returns results in the same order as requests, regardless of completion order
func (o *Orchestrator) All(reqs []*Request) []*Result {
if len(reqs) == 0 {
return []*Result{}
}
results := make([]*Result, len(reqs))
var wg sync.WaitGroup
var mu sync.Mutex
for i, req := range reqs {
wg.Add(1)
go func(idx int, r *Request) {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
mu.Lock()
results[idx] = &Result{
AgentID: r.AgentID,
Error: "agent call panic recovered",
}
mu.Unlock()
}
}()
result := o.callAgent(r)
mu.Lock()
results[idx] = result
mu.Unlock()
}(i, req)
}
wg.Wait()
return results
}
// Any returns as soon as any agent call succeeds (has non-error result) (like Promise.any)
// Other calls continue in background but results are discarded after first success
// Returns all results received so far when first success is found
func (o *Orchestrator) Any(reqs []*Request) []*Result {
if len(reqs) == 0 {
return []*Result{}
}
results := make([]*Result, len(reqs))
resultChan := make(chan callResult, len(reqs))
var wg sync.WaitGroup
done := make(chan struct{})
for i, req := range reqs {
wg.Add(1)
go func(idx int, r *Request) {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
// Send panic result through channel
select {
case <-done:
case resultChan <- callResult{idx: idx, result: &Result{
AgentID: r.AgentID,
Error: "agent call panic recovered",
}}:
}
}
}()
// Check if done before starting
select {
case <-done:
return
default:
}
result := o.callAgent(r)
// Try to send result
select {
case <-done:
// Already found a successful result
case resultChan <- callResult{idx: idx, result: result}:
}
}(i, req)
}
// Close channel when all goroutines complete
go func() {
wg.Wait()
close(resultChan)
}()
// Collect results until we find one with success (no error and has content)
var foundSuccess bool
for res := range resultChan {
results[res.idx] = res.result
// Check if this result is successful (no error)
if !foundSuccess && res.result != nil && res.result.Error == "" {
foundSuccess = true
close(done) // Signal other goroutines to stop
}
}
return results
}
// Race returns as soon as any agent call completes (like Promise.race)
// Returns immediately when first result arrives, regardless of success/failure
// Note: Still waits for all goroutines to complete before returning to avoid resource leaks
func (o *Orchestrator) Race(reqs []*Request) []*Result {
if len(reqs) == 0 {
return []*Result{}
}
results := make([]*Result, len(reqs))
resultChan := make(chan callResult, len(reqs))
var wg sync.WaitGroup
done := make(chan struct{})
for i, req := range reqs {
wg.Add(1)
go func(idx int, r *Request) {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
// Send panic result through channel
select {
case <-done:
case resultChan <- callResult{idx: idx, result: &Result{
AgentID: r.AgentID,
Error: "agent call panic recovered",
}}:
}
}
}()
// Check if done before starting
select {
case <-done:
return
default:
}
result := o.callAgent(r)
// Try to send result
select {
case <-done:
// Already got first result
case resultChan <- callResult{idx: idx, result: result}:
}
}(i, req)
}
// Close channel when all goroutines complete
go func() {
wg.Wait()
close(resultChan)
}()
// Get first result and signal others to stop
var gotFirst bool
for res := range resultChan {
results[res.idx] = res.result
if !gotFirst {
gotFirst = true
close(done) // Signal other goroutines to stop
}
}
return results
}
// callAgent executes a single agent call using the AgentGetterFunc
// This method handles context sharing and result extraction
func (o *Orchestrator) callAgent(req *Request) *Result {
if req == nil {
return &Result{Error: "nil request"}
}
result := &Result{
AgentID: req.AgentID,
}
// Get the agent using the getter function
if AgentGetterFunc == nil {
result.Error = "agent getter not initialized"
return result
}
agent, err := AgentGetterFunc(req.AgentID)
if err != nil {
result.Error = "failed to get agent: " + err.Error()
return result
}
// Build context options for the call
var ctxOpts *agentContext.Options
if req.Options != nil {
ctxOpts = req.Options.ToContextOptions()
} else {
ctxOpts = &agentContext.Options{}
}
// If request has a handler, set OnMessage callback
if req.Handler != nil {
if ctxOpts == nil {
ctxOpts = &agentContext.Options{}
}
// Set OnMessage to receive SSE messages
ctxOpts.OnMessage = req.Handler
}
// Execute the agent call with shared context
// The agent.Stream method will use the parent context's Writer for output
resp, err := agent.Stream(o.ctx, req.Messages, ctxOpts)
if err != nil {
result.Error = "agent call failed: " + err.Error()
return result
}
result.Response = resp
// Extract content from completion if available
if resp != nil && resp.Completion != nil {
result.Content = extractContentFromCompletion(resp.Completion)
}
return result
}
// extractContentFromCompletion extracts the text content from a completion response
func extractContentFromCompletion(completion *agentContext.CompletionResponse) string {
if completion == nil {
return ""
}
// Content can be string or []ContentPart
switch content := completion.Content.(type) {
case string:
return content
case []interface{}:
// Handle array of content parts - extract text parts
var texts []string
for _, part := range content {
if partMap, ok := part.(map[string]interface{}); ok {
if partType, ok := partMap["type"].(string); ok && partType == "text" {
if text, ok := partMap["text"].(string); ok {
texts = append(texts, text)
}
}
}
}
if len(texts) > 0 {
return texts[0] // Return first text content
}
}
return ""
}