- 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.
286 lines
6.6 KiB
Go
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 ""
|
|
}
|