Calling Reasoners

Execute reasoners with automatic parent-child workflow tracking

Calling Reasoners

Execute reasoners across agents with automatic parent-child workflow relationships

The agent.Call() method executes reasoners via the Agentfield control plane and automatically creates parent-child workflow relationships for visualization and tracking.

Zero-Configuration Workflow Tracking: Parent-child relationships are created automatically. You don't need to manually track execution hierarchies or build DAGs - the SDK and Agentfield control plane handle everything.

Basic Usage

result, err := app.Call(ctx, "agent-name.reasoner-name", map[string]any{
    "input_key": "input_value",
})
if err != nil {
    log.Fatal(err)
}

// result is map[string]any containing the reasoner's response
fmt.Printf("Result: %v\n", result["output_key"])

Method Signature

func (a *Agent) Call(
    ctx context.Context,
    target string,
    input map[string]any,
) (map[string]any, error)

Parameters

  • ctx: Context for cancellation and timeouts
  • target: Reasoner identifier in format "agent-id.reasoner-name"
  • input: Input data as a map (must be JSON-serializable)

Target Format

The target string specifies which reasoner to call:

// Call another agent's reasoner
result, err := app.Call(ctx, "data-processor.transform", input)

// Call own reasoner (creates parent-child relationship)
result, err := app.Call(ctx, "my-agent.validate", input)

// Call reasoner on different team (if multi-tenant)
result, err := app.Call(ctx, "team-b-agent.process", input)

Automatic Workflow Tracking

When you call a reasoner from within another reasoner, the SDK automatically:

  1. Extracts current execution context from ctx
  2. Sets X-Parent-Execution-ID header
  3. Agentfield creates parent-child relationship in database
  4. Builds workflow DAG for UI visualization

Example: Automatic Parent-Child

// Parent reasoner
app.RegisterReasoner("orchestrate", func(ctx context.Context, input map[string]any) (any, error) {
    log.Println("Starting orchestration...")

    // Call child reasoner - automatically creates parent-child link
    result1, err := app.Call(ctx, "my-agent.analyze", input)
    if err != nil {
        return nil, err
    }

    // Another child - also linked to parent "orchestrate"
    result2, err := app.Call(ctx, "my-agent.summarize", result1)
    if err != nil {
        return nil, err
    }

    return map[string]any{
        "analysis": result1,
        "summary":  result2,
    }, nil
})

Agentfield automatically tracks:

orchestrate (parent)
  ├─> analyze (child)
  └─> summarize (child)

Common Patterns

Sequential Workflow

Execute reasoners one after another:

app.RegisterReasoner("pipeline", func(ctx context.Context, input map[string]any) (any, error) {
    // Step 1: Extract
    extracted, err := app.Call(ctx, "my-agent.extract", input)
    if err != nil {
        return nil, fmt.Errorf("extract failed: %w", err)
    }

    // Step 2: Transform (uses result from step 1)
    transformed, err := app.Call(ctx, "my-agent.transform", extracted)
    if err != nil {
        return nil, fmt.Errorf("transform failed: %w", err)
    }

    // Step 3: Load
    loaded, err := app.Call(ctx, "my-agent.load", transformed)
    if err != nil {
        return nil, fmt.Errorf("load failed: %w", err)
    }

    // Agentfield tracks: pipeline -> extract -> transform -> load
    return loaded, nil
})

Parallel Execution

Use goroutines for concurrent reasoner calls:

app.RegisterReasoner("parallel_process", func(ctx context.Context, input map[string]any) (any, error) {
    var wg sync.WaitGroup
    results := make([]map[string]any, 4)
    errors := make([]error, 4)

    tasks := []string{"task1", "task2", "task3", "task4"}

    for i, task := range tasks {
        wg.Add(1)
        go func(index int, taskName string) {
            defer wg.Done()

            // Each call creates its own child execution
            result, err := app.Call(ctx, fmt.Sprintf("my-agent.%s", taskName), input)
            if err != nil {
                errors[index] = err
                return
            }
            results[index] = result
        }(i, task)
    }

    wg.Wait()

    // Check for errors
    for i, err := range errors {
        if err != nil {
            return nil, fmt.Errorf("task %d failed: %w", i, err)
        }
    }

    // Agentfield tracks all 4 as parallel children
    return map[string]any{"results": results}, nil
})

Agentfield visualizes:

parallel_process (parent)
  ├─> task1 (child)
  ├─> task2 (child)
  ├─> task3 (child)
  └─> task4 (child)

Conditional Execution

Execute different reasoners based on logic:

app.RegisterReasoner("conditional_flow", func(ctx context.Context, input map[string]any) (any, error) {
    dataType := input["type"].(string)

    var result map[string]any
    var err error

    switch dataType {
    case "text":
        result, err = app.Call(ctx, "my-agent.process_text", input)
    case "image":
        result, err = app.Call(ctx, "my-agent.process_image", input)
    case "audio":
        result, err = app.Call(ctx, "my-agent.process_audio", input)
    default:
        return nil, fmt.Errorf("unknown type: %s", dataType)
    }

    if err != nil {
        return nil, err
    }

    // Agentfield tracks which path was taken
    return result, nil
})

Multi-Agent Orchestration

Call reasoners across different agents:

app.RegisterReasoner("cross_agent_workflow", func(ctx context.Context, input map[string]any) (any, error) {
    // Step 1: Data extraction agent
    extracted, err := app.Call(ctx, "data-extractor.extract", input)
    if err != nil {
        return nil, err
    }

    // Step 2: AI analysis agent
    analyzed, err := app.Call(ctx, "ai-analyzer.analyze", extracted)
    if err != nil {
        return nil, err
    }

    // Step 3: Reporting agent
    report, err := app.Call(ctx, "reporter.generate", analyzed)
    if err != nil {
        return nil, err
    }

    // Agentfield tracks cross-agent workflow
    return report, nil
})

Error Recovery

Implement fallback logic:

app.RegisterReasoner("resilient_call", func(ctx context.Context, input map[string]any) (any, error) {
    // Try primary reasoner
    result, err := app.Call(ctx, "my-agent.primary_processor", input)
    if err == nil {
        return result, nil
    }

    log.Printf("Primary failed: %v, trying fallback...", err)

    // Fallback to simpler processor
    result, err = app.Call(ctx, "my-agent.fallback_processor", input)
    if err != nil {
        return nil, fmt.Errorf("both primary and fallback failed: %w", err)
    }

    return map[string]any{
        "result":   result,
        "fallback": true,
    }, nil
})

Execution Context

The ctx parameter carries execution context through the call chain. This context is automatically extracted and used to set parent-child relationships.

Context Propagation

// The context flows through the entire chain
func handler1(ctx context.Context, input map[string]any) (any, error) {
    // ctx contains execution metadata
    // SDK extracts ExecutionID automatically

    // When we call another reasoner, ctx is used to set parent relationship
    return app.Call(ctx, "my-agent.handler2", input)
}

func handler2(ctx context.Context, input map[string]any) (any, error) {
    // This execution knows its parent is handler1
    // All automatic - no manual tracking needed
    return processData(input)
}

Timeouts and Cancellation

Use context for timeout control:

app.RegisterReasoner("with_timeout", func(ctx context.Context, input map[string]any) (any, error) {
    // Create timeout context for child call
    timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    result, err := app.Call(timeoutCtx, "my-agent.slow_task", input)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("task timed out after 10s")
        }
        return nil, err
    }

    return result, nil
})

Best Practices

Always Pass Context

// ✅ Good - uses context from parameters
app.RegisterReasoner("handler", func(ctx context.Context, input map[string]any) (any, error) {
    return app.Call(ctx, "my-agent.worker", input)
})

// ❌ Bad - creates new context (breaks workflow tracking)
app.RegisterReasoner("handler", func(ctx context.Context, input map[string]any) (any, error) {
    newCtx := context.Background()  // Don't do this!
    return app.Call(newCtx, "my-agent.worker", input)
})

Handle Errors Gracefully

// ✅ Good - handles errors with context
result, err := app.Call(ctx, "my-agent.risky", input)
if err != nil {
    log.Printf("Reasoner call failed: %v", err)
    return map[string]any{
        "status": "failed",
        "error":  err.Error(),
    }, nil  // Return structured error, don't propagate
}

Use Structured Input/Output

// ✅ Good - clear structure
input := map[string]any{
    "operation": "analyze",
    "data":      data,
    "options": map[string]any{
        "detailed": true,
        "format":   "json",
    },
}

result, err := app.Call(ctx, "my-agent.process", input)

Workflow Visualization

All reasoner calls are automatically tracked and visualized in the Agentfield UI:

  1. Navigate to Workflow tab in Agentfield UI
  2. Select your execution run
  3. See the complete DAG with:
    • Parent-child relationships
    • Execution times
    • Status (success/failed)
    • Input/output data

The SDK sets these headers automatically:

  • X-Run-ID: Groups related executions
  • X-Execution-ID: Unique ID for this execution
  • X-Parent-Execution-ID: Links to parent execution

Agentfield uses these to build the workflow DAG.