Registering Reasoners

Define and register reasoners that Agentfield can execute

Registering Reasoners

Define executable reasoners that can be called by Agentfield or other agents

Reasoners are the core execution units in Agentfield. Register them with your agent to make them callable via the Agentfield control plane.

Basic Registration

app.RegisterReasoner("reasoner_name", func(ctx context.Context, input map[string]any) (any, error) {
    // Process input
    data := input["key"].(string)

    // Return result
    return map[string]any{
        "result": "processed: " + data,
    }, nil
})

Method Signature

func (a *Agent) RegisterReasoner(
    name string,
    handler HandlerFunc,
    opts ...ReasonerOption,
)

type HandlerFunc func(context.Context, map[string]any) (any, error)

Handler Requirements

Your handler function must:

  1. Accept context.Context as first parameter
  2. Accept map[string]any as input
  3. Return (any, error)
  4. Return JSON-serializable types

The returned value must be JSON-serializable. Use map[string]any, structs with JSON tags, or primitive types.

Common Patterns

Simple Processing

app.RegisterReasoner("greet", func(ctx context.Context, input map[string]any) (any, error) {
    name, ok := input["name"].(string)
    if !ok {
        return nil, fmt.Errorf("name is required")
    }

    return map[string]any{
        "message": fmt.Sprintf("Hello, %s!", name),
    }, nil
})

With AI Integration

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

    // Use AI for intelligent processing
    response, err := app.AI(ctx,
        fmt.Sprintf("Analyze this text: %s", text),
        ai.WithSchema(Analysis{}))
    if err != nil {
        return nil, err
    }

    var analysis Analysis
    response.Into(&analysis)

    return map[string]any{
        "analysis": analysis,
        "tokens":   response.Usage.TotalTokens,
    }, nil
})

Calling Other Reasoners

app.RegisterReasoner("orchestrate", func(ctx context.Context, input map[string]any) (any, error) {
    // Call child reasoners
    result1, err := app.Call(ctx, "my-agent.step1", input)
    if err != nil {
        return nil, err
    }

    result2, err := app.Call(ctx, "my-agent.step2", result1)
    if err != nil {
        return nil, err
    }

    return result2, nil
})

Input Validation

app.RegisterReasoner("process_order", func(ctx context.Context, input map[string]any) (any, error) {
    // Validate required fields
    orderID, ok := input["order_id"].(string)
    if !ok || orderID == "" {
        return nil, fmt.Errorf("order_id is required")
    }

    amount, ok := input["amount"].(float64)
    if !ok || amount <= 0 {
        return nil, fmt.Errorf("amount must be positive number")
    }

    // Process order
    return map[string]any{
        "order_id": orderID,
        "status":   "processed",
        "total":    amount * 1.1, // Add tax
    }, nil
})

Reasoner Options

CLI Options

Enable reasoners as CLI commands for dual-mode binaries.

WithCLI()

Expose this reasoner as a CLI command:

app.RegisterReasoner("greet", greetHandler, agent.WithCLI())
// Usage: ./binary greet --set name=Alice

WithDefaultCLI()

Make this reasoner the default CLI command (must be used with WithCLI()):

app.RegisterReasoner("process", processHandler,
    agent.WithCLI(),
    agent.WithDefaultCLI(),
)
// Usage: ./binary --set input=data  (no command name needed)

WithCLIFormatter(fn)

Customize CLI output formatting:

app.RegisterReasoner("analyze", analyzeHandler,
    agent.WithCLI(),
    agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
        if err != nil {
            fmt.Fprintf(os.Stderr, "❌ Error: %v\n", err)
            os.Exit(1)
            return
        }

        data := result.(map[string]any)
        fmt.Printf("✓ Sentiment: %s (%.1f%% confidence)\n",
            data["sentiment"], data["confidence"].(float64)*100)
    }),
)

WithDescription(desc)

Add description for help text:

app.RegisterReasoner("analyze", handler,
    agent.WithCLI(),
    agent.WithDescription("Analyze text with AI"),
)

Complete CLI Example:

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

    // Detect CLI mode for custom behavior
    if agent.IsCLIMode(ctx) {
        return fmt.Sprintf("Hello, %s!", name), nil
    }

    // Server mode: structured response
    return map[string]any{
        "greeting": fmt.Sprintf("Hello, %s!", name),
        "timestamp": time.Now().Unix(),
    }, nil
},
    agent.WithCLI(),
    agent.WithDefaultCLI(),
    agent.WithDescription("Greet a user by name"),
)

CLI Input Methods:

  • --set key=value - Individual parameters (repeatable)
  • --input '{"key":"value"}' - JSON string
  • --input-file data.json - JSON file
  • Stdin pipe (implicit)

Priority: --set > --input > --input-file > stdin

See CLI-Enabled Reasoners for complete documentation.

Best Practices

Return Structured Data

// ✅ Good - structured response
return map[string]any{
    "status": "success",
    "data":   result,
    "meta": map[string]any{
        "processed_at": time.Now().Unix(),
        "version":      "1.0",
    },
}, nil

// ❌ Bad - unclear structure
return result, nil

Handle Errors Gracefully

// ✅ Good - returns error
if err != nil {
    return nil, fmt.Errorf("processing failed: %w", err)
}

// ✅ Also good - returns structured error response
if err != nil {
    return map[string]any{
        "status": "error",
        "error":  err.Error(),
        "code":   "PROCESSING_FAILED",
    }, nil
}

Use Context

// ✅ Good - respects context cancellation
select {
case <-ctx.Done():
    return nil, ctx.Err()
case result := <-resultChan:
    return result, nil
}