Agent Discovery

Discover available agent capabilities and schemas at runtime for dynamic orchestration

Agent Discovery

Discover available agent capabilities and schemas at runtime for dynamic orchestration

Discover available agents, reasoners, and skills at runtime with their input/output schemas. Perfect for building AI orchestrators that select tools dynamically, generating LLM tool catalogs, or implementing intelligent health-based routing.

Basic Example

package main

import (
    "context"
    "fmt"
    "github.com/Agent-Field/agentfield/sdk/go/agent"
)

func main() {
    app, _ := agent.New(agent.Config{
        NodeID:        "orchestrator",
        AgentFieldURL: "http://localhost:8080",
    })

    ctx := context.Background()

    // Discover all research-related capabilities
    capabilities, err := app.Discover(
        ctx,
        agent.WithTags([]string{"research"}),
        agent.WithInputSchema(true),
    )

    if err != nil {
        panic(err)
    }

    fmt.Printf("Found %d agents with %d reasoners\n",
        capabilities.TotalAgents,
        capabilities.TotalReasoners)

    // Use discovered capabilities
    for _, agentCap := range capabilities.Capabilities {
        for _, reasoner := range agentCap.Reasoners {
            fmt.Printf("Found: %s\n", reasoner.InvocationTarget)
        }
    }
}

Discovery Options

Use functional options to filter and configure discovery:

Prop

Type

Discovery Response Types

type DiscoveryResponse struct {
    DiscoveredAt   time.Time          `json:"discovered_at"`
    TotalAgents    int                `json:"total_agents"`
    TotalReasoners int                `json:"total_reasoners"`
    TotalSkills    int                `json:"total_skills"`
    Pagination     Pagination         `json:"pagination"`
    Capabilities   []AgentCapability  `json:"capabilities"`
}

type AgentCapability struct {
    AgentID        string              `json:"agent_id"`
    BaseURL        string              `json:"base_url"`
    Version        string              `json:"version"`
    HealthStatus   string              `json:"health_status"`
    DeploymentType string              `json:"deployment_type"`
    LastHeartbeat  time.Time           `json:"last_heartbeat"`
    Reasoners      []ReasonerCapability `json:"reasoners"`
    Skills         []SkillCapability    `json:"skills"`
}

type ReasonerCapability struct {
    ID               string                 `json:"id"`
    Description      *string                `json:"description,omitempty"`
    Tags             []string               `json:"tags"`
    InputSchema      map[string]interface{} `json:"input_schema,omitempty"`
    OutputSchema     map[string]interface{} `json:"output_schema,omitempty"`
    Examples         []map[string]interface{} `json:"examples,omitempty"`
    InvocationTarget string                 `json:"invocation_target"`
}

type SkillCapability struct {
    ID               string                 `json:"id"`
    Description      *string                `json:"description,omitempty"`
    Tags             []string               `json:"tags"`
    InputSchema      map[string]interface{} `json:"input_schema,omitempty"`
    InvocationTarget string                 `json:"invocation_target"`
}

type Pagination struct {
    Limit   int  `json:"limit"`
    Offset  int  `json:"offset"`
    HasMore bool `json:"has_more"`
}

Common Patterns

Basic Discovery

Discover all available capabilities:

package main

import (
    "context"
    "fmt"
    "github.com/Agent-Field/agentfield/sdk/go/agent"
)

func main() {
    app, _ := agent.New(agent.Config{
        NodeID:        "my-agent",
        AgentFieldURL: "http://localhost:8080",
    })

    ctx := context.Background()

    // Discover everything
    capabilities, err := app.Discover(ctx)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Found %d agents\n", capabilities.TotalAgents)
    fmt.Printf("Found %d reasoners\n", capabilities.TotalReasoners)
    fmt.Printf("Found %d skills\n", capabilities.TotalSkills)

    // Iterate through agents
    for _, agentCap := range capabilities.Capabilities {
        fmt.Printf("Agent: %s (v%s)\n", agentCap.AgentID, agentCap.Version)
        fmt.Printf("  Health: %s\n", agentCap.HealthStatus)

        for _, reasoner := range agentCap.Reasoners {
            fmt.Printf("  Reasoner: %s\n", reasoner.ID)
            fmt.Printf("    Target: %s\n", reasoner.InvocationTarget)
            fmt.Printf("    Tags: %v\n", reasoner.Tags)
        }
    }
}

Filter by Wildcards

Use wildcard patterns to find specific capabilities:

// Find all research-related reasoners
researchCaps, err := app.Discover(
    ctx,
    agent.WithReasonerPattern("*research*"),
    agent.WithInputSchema(true),
)

// Find reasoners starting with "deep_"
deepCaps, err := app.Discover(
    ctx,
    agent.WithReasonerPattern("deep_*"),
)

// Find web-related skills
webSkills, err := app.Discover(
    ctx,
    agent.WithSkillPattern("web_*"),
    agent.WithTags([]string{"web", "scraping"}),
)

Wildcard Patterns:

  • *abc* - Contains "abc" anywhere
  • abc* - Starts with "abc"
  • *abc - Ends with "abc"
  • abc - Exact match

Filter by Tags

Find capabilities using tags with wildcard support:

// Find ML-related capabilities (with wildcard)
mlCaps, err := app.Discover(
    ctx,
    agent.WithTags([]string{"ml*"}), // Matches: ml, mlops, ml_vision
    agent.WithHealthStatus("active"),
)

// Find capabilities with multiple tags
multiTagCaps, err := app.Discover(
    ctx,
    agent.WithTags([]string{"research", "nlp", "ml"}),
    agent.WithInputSchema(true),
)

Schema-Based Discovery

Discover capabilities with full schemas for validation:

// Get full schemas for dynamic validation
capabilities, err := app.Discover(
    ctx,
    agent.WithTags([]string{"data"}),
    agent.WithInputSchema(true),
    agent.WithOutputSchema(true),
)

if err != nil {
    panic(err)
}

for _, agentCap := range capabilities.Capabilities {
    for _, reasoner := range agentCap.Reasoners {
        fmt.Printf("Reasoner: %s\n", reasoner.ID)
        fmt.Printf("Input Schema: %+v\n", reasoner.InputSchema)
        fmt.Printf("Output Schema: %+v\n", reasoner.OutputSchema)

        // Validate user input against schema
        if ValidatesAgainstSchema(userInput, reasoner.InputSchema) {
            result, _ := app.Execute(ctx, agent.ExecuteRequest{
                Target: reasoner.InvocationTarget,
                Input:  userInput,
            })
            fmt.Printf("Result: %+v\n", result)
        }
    }
}

AI Orchestrator Pattern

Build AI systems that select tools at runtime:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/Agent-Field/agentfield/sdk/go/agent"
)

type Tool struct {
    Name             string                 `json:"name"`
    Description      string                 `json:"description"`
    Parameters       map[string]interface{} `json:"parameters"`
    InvocationTarget string                 `json:"-"`
}

func AIOrchestrator(ctx context.Context, app *agent.Agent, userRequest string) (map[string]interface{}, error) {
    // Step 1: Discover available tools
    capabilities, err := app.Discover(
        ctx,
        agent.WithInputSchema(true),
        agent.WithDescriptions(true),
    )
    if err != nil {
        return nil, err
    }

    // Step 2: Build tool catalog for AI
    tools := []Tool{}
    for _, agentCap := range capabilities.Capabilities {
        for _, reasoner := range agentCap.Reasoners {
            tool := Tool{
                Name:             reasoner.ID,
                Description:      *reasoner.Description,
                Parameters:       reasoner.InputSchema,
                InvocationTarget: reasoner.InvocationTarget,
            }
            tools = append(tools, tool)
        }
    }

    // Step 3: Let AI select the right tool
    selectedTool, args := AISelectTool(userRequest, tools) // Your AI logic

    // Step 4: Execute selected tool
    result, err := app.Execute(ctx, agent.ExecuteRequest{
        Target: selectedTool.InvocationTarget,
        Input:  args,
    })

    return result, err
}

func main() {
    app, _ := agent.New(agent.Config{
        NodeID:        "orchestrator",
        AgentFieldURL: "http://localhost:8080",
    })

    ctx := context.Background()

    result, err := AIOrchestrator(ctx, app, "Research AI trends in 2025")
    if err != nil {
        panic(err)
    }

    fmt.Printf("Result: %+v\n", result)
}

Health-Based Routing

Route to healthy agents with automatic failover:

func ResilientExecution(ctx context.Context, app *agent.Agent, task string) (map[string]interface{}, error) {
    // Find healthy agents with the capability
    healthyCaps, err := app.Discover(
        ctx,
        agent.WithTags([]string{"processing"}),
        agent.WithHealthStatus("active"),
    )

    if err != nil {
        return nil, err
    }

    if len(healthyCaps.Capabilities) == 0 {
        return nil, fmt.Errorf("no healthy agents available")
    }

    // Try each healthy agent until one succeeds
    var lastErr error
    for _, agentCap := range healthyCaps.Capabilities {
        for _, reasoner := range agentCap.Reasoners {
            result, err := app.Execute(ctx, agent.ExecuteRequest{
                Target: reasoner.InvocationTarget,
                Input:  map[string]interface{}{"task": task},
            })

            if err == nil {
                return map[string]interface{}{
                    "result":      result,
                    "executed_by": agentCap.AgentID,
                }, nil
            }

            lastErr = err
            // Continue to next agent
        }
    }

    return nil, fmt.Errorf("all agents failed: %w", lastErr)
}

Compact Format for Performance

Use compact format when you only need targets:

// Minimal response, just IDs and targets
compactCaps, err := app.Discover(
    ctx,
    agent.WithFormat("compact"),
)

if err != nil {
    panic(err)
}

// Compact format returns minimal structure
// Access via type assertion
compactData := compactCaps.(*CompactDiscoveryResponse)

// Fast lookup by tag
for _, reasoner := range compactData.Reasoners {
    for _, tag := range reasoner.Tags {
        if tag == "search" {
            fmt.Printf("Search target: %s\n", reasoner.Target)
        }
    }
}

XML Format for LLM Context

Use XML format for LLM system prompts:

// Get capabilities in XML format
xmlCaps, err := app.Discover(
    ctx,
    agent.WithFormat("xml"),
    agent.WithDescriptions(true),
)

if err != nil {
    panic(err)
}

// XML response is returned as string in XMLDiscoveryResponse
xmlData := xmlCaps.(*XMLDiscoveryResponse)

// Inject into LLM system prompt
systemPrompt := fmt.Sprintf(`
You are an AI assistant with access to these capabilities:

%s

Select and use the appropriate capability for the user's request.
`, xmlData.XML)

// Use with any LLM
response := CallLLM(systemPrompt, userQuery)

Advanced Patterns

Best Practices

Cache Discovery Results

Discovery results change infrequently—cache them:

import (
    "sync"
    "time"
)

type CapabilityCache struct {
    cache      *DiscoveryResponse
    cacheTime  time.Time
    ttl        time.Duration
    mu         sync.RWMutex
}

func NewCapabilityCache(ttl time.Duration) *CapabilityCache {
    return &CapabilityCache{
        ttl: ttl,
    }
}

func (c *CapabilityCache) GetCapabilities(
    ctx context.Context,
    app *agent.Agent,
    options ...agent.DiscoverOption,
) (*DiscoveryResponse, error) {
    c.mu.RLock()
    if c.cache != nil && time.Since(c.cacheTime) < c.ttl {
        defer c.mu.RUnlock()
        return c.cache, nil
    }
    c.mu.RUnlock()

    // Refresh cache
    capabilities, err := app.Discover(ctx, options...)
    if err != nil {
        return nil, err
    }

    c.mu.Lock()
    c.cache = capabilities
    c.cacheTime = time.Now()
    c.mu.Unlock()

    return capabilities, nil
}

// Usage
cache := NewCapabilityCache(30 * time.Second)

func MyReasoner(ctx context.Context, app *agent.Agent) {
    capabilities, _ := cache.GetCapabilities(
        ctx,
        app,
        agent.WithTags([]string{"research"}),
    )
    // Use capabilities...
}

Filter Aggressively

Request only what you need to minimize response size and latency:

// Bad: Retrieve everything
allCaps, _ := app.Discover(ctx) // Large response, slow

// Good: Filter to specific needs
focusedCaps, _ := app.Discover(
    ctx,
    agent.WithTags([]string{"research"}),
    agent.WithHealthStatus("active"),
    agent.WithInputSchema(true),
    agent.WithFormat("compact"),
) // Small response, fast

Handle Errors Gracefully

Always handle the case where no capabilities are found:

func SafeDiscovery(ctx context.Context, app *agent.Agent, capabilityType string) error {
    capabilities, err := app.Discover(
        ctx,
        agent.WithTags([]string{capabilityType}),
    )

    if err != nil {
        return fmt.Errorf("discovery failed: %w", err)
    }

    if len(capabilities.Capabilities) == 0 {
        return fmt.Errorf(
            "no capabilities found for type: %s. Check if agents are running",
            capabilityType,
        )
    }

    fmt.Printf("Found %d agents\n", len(capabilities.Capabilities))
    return nil
}

Performance Considerations

Caching:

  • Server caches results for 30 seconds
  • Typical cache hit rate: >95%
  • Response time: <50ms (p50), <100ms (p95)

Schema Inclusion:

  • Without schemas: ~10-20KB response
  • With input/output schemas: ~50-100KB response
  • Use WithInputSchema(true) only when needed

Filtering:

  • All filtering happens server-side in-memory
  • Wildcard matching adds ~5-10ms overhead
  • Tag filtering is highly optimized

Best Performance:

// Fast: Minimal response
fastDiscovery, _ := app.Discover(
    ctx,
    agent.WithTags([]string{"specific"}),
    agent.WithFormat("compact"),
    agent.WithDescriptions(false),
)

// Slower: Full response with schemas
fullDiscovery, _ := app.Discover(
    ctx,
    agent.WithInputSchema(true),
    agent.WithOutputSchema(true),
    agent.WithExamples(true),
)