Output Formatting

Customize CLI output with default formats and custom formatters

Default Output Formats

The SDK provides three built-in output formats controlled by the --output flag:

FormatDescriptionUse Case
jsonPretty-printed JSONDefault, machine-readable
yamlYAML formatConfiguration output
textPlain textSimple string results

JSON Output (Default)

./binary greet --set name=Alice
{
  "greeting": "Hello, Alice!",
  "timestamp": 1700000000
}

YAML Output

./binary greet --set name=Alice --output yaml
greeting: Hello, Alice!
timestamp: 1700000000

Text Output

Best for simple string results:

./binary greet --set name=Alice --output text
Hello, Alice!

Custom Formatters

Use WithCLIFormatter() to customize output for specific reasoners.

Basic Formatter

a.RegisterReasoner("greet", greetHandler,
    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
        }

        fmt.Println(result)
    }),
)

Formatter with Type Assertion

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

        data, ok := result.(map[string]any)
        if !ok {
            fmt.Fprintf(os.Stderr, "Unexpected result type\n")
            os.Exit(1)
            return
        }

        fmt.Println("📊 Analysis Results")
        fmt.Println("──────────────────")
        fmt.Printf("Sentiment: %s\n", data["sentiment"])
        fmt.Printf("Confidence: %.2f%%\n", data["confidence"].(float64)*100)
        fmt.Printf("Key topics: %v\n", data["topics"])
    }),
)

Output:

📊 Analysis Results
──────────────────
Sentiment: positive
Confidence: 87.50%
Key topics: [technology innovation growth]

Color Output

Use color for better readability (respects NO_COLOR environment variable):

import "github.com/fatih/color"  // Popular color library

a.RegisterReasoner("status", statusHandler,
    agent.WithCLI(),
    agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
        if err != nil {
            color.Red("✗ Error: %v", err)
            os.Exit(1)
            return
        }

        data := result.(map[string]any)
        status := data["status"].(string)

        switch status {
        case "success":
            color.Green("✓ %s", data["message"])
        case "warning":
            color.Yellow("⚠ %s", data["message"])
        case "error":
            color.Red("✗ %s", data["message"])
        }
    }),
)

Respecting CLI Config

Access CLI config to respect color settings:

a.RegisterReasoner("greet", greetHandler,
    agent.WithCLI(),
    agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
        // Colors automatically disabled if DisableColors: true
        // or if NO_COLOR env var is set

        if err != nil {
            color.Red("Error: %v", err)
            os.Exit(1)
            return
        }

        color.Green("Success: %v", result)
    }),
)

Structured Table Output

Format results as tables for better readability:

import "github.com/olekukonko/tablewriter"

a.RegisterReasoner("list", listHandler,
    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
        }

        items := result.([]map[string]any)

        table := tablewriter.NewWriter(os.Stdout)
        table.SetHeader([]string{"ID", "Name", "Status"})

        for _, item := range items {
            table.Append([]string{
                fmt.Sprintf("%v", item["id"]),
                fmt.Sprintf("%v", item["name"]),
                fmt.Sprintf("%v", item["status"]),
            })
        }

        table.Render()
    }),
)

Output:

+----+----------+---------+
| ID |   NAME   | STATUS  |
+----+----------+---------+
|  1 | Alice    | active  |
|  2 | Bob      | pending |
|  3 | Charlie  | active  |
+----+----------+---------+

Progress Indicators

Show progress for long-running operations:

import "github.com/schollz/progressbar/v3"

a.RegisterReasoner("process", func(ctx context.Context, input map[string]any) (any, error) {
    items := input["items"].([]any)

    if agent.IsCLIMode(ctx) {
        bar := progressbar.Default(int64(len(items)))

        for _, item := range items {
            // Process item
            processItem(item)
            bar.Add(1)
        }
    } else {
        // Server mode: no progress bar
        for _, item := range items {
            processItem(item)
        }
    }

    return map[string]any{"processed": len(items)}, nil
}, agent.WithCLI())

Error Formatting

Simple Error Messages

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
    }

    fmt.Println(result)
})

Detailed Error Messages

agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "❌ Operation failed\n\n")
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)

        // Additional context
        if strings.Contains(err.Error(), "API key") {
            fmt.Fprintf(os.Stderr, "\nHint: Set OPENAI_API_KEY environment variable\n")
        }

        os.Exit(1)
        return
    }

    // Success output
    fmt.Printf("✓ %v\n", result)
})

Exit Codes

Use meaningful exit codes:

agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)

        // Different exit codes for different errors
        if errors.Is(err, ErrNotFound) {
            os.Exit(2)  // Not found
        } else if errors.Is(err, ErrPermission) {
            os.Exit(3)  // Permission denied
        } else {
            os.Exit(1)  // General error
        }
        return
    }

    // Success
    fmt.Println(result)
    os.Exit(0)
})

Common exit codes:

  • 0 - Success
  • 1 - General error
  • 2 - Misuse / invalid input
  • 3 - Permission error
  • 4 - Not found

Streaming Output

For streaming results, write directly to stdout:

a.RegisterReasoner("stream", func(ctx context.Context, input map[string]any) (any, error) {
    if agent.IsCLIMode(ctx) {
        // Stream output directly
        for i := 0; i < 10; i++ {
            fmt.Printf("Processing chunk %d...\n", i)
            time.Sleep(100 * time.Millisecond)
        }
        return "Streaming complete", nil
    }

    // Server mode: return structured result
    return map[string]any{"chunks": 10}, nil
}, agent.WithCLI())

JSON Lines Output

For processing pipelines:

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
    }

    items := result.([]map[string]any)

    // Output as JSON Lines (one JSON object per line)
    for _, item := range items {
        jsonBytes, _ := json.Marshal(item)
        fmt.Println(string(jsonBytes))
    }
})

Usage in pipeline:

./binary list | jq '.name' | sort

Conditional Formatting

Different formats for different contexts:

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
    }

    // Check if output is a TTY (interactive terminal)
    if isatty.IsTerminal(os.Stdout.Fd()) {
        // Interactive: use colors and formatting
        color.Green("✓ Success")
        fmt.Printf("Result: %v\n", result)
    } else {
        // Piped: machine-readable output
        jsonBytes, _ := json.Marshal(result)
        fmt.Println(string(jsonBytes))
    }
})

Best Practices

Human vs Machine Output

// Good: Detect context
if agent.IsCLIMode(ctx) {
    // Human-friendly
    return "Operation successful!", nil
}
// Machine-readable
return map[string]any{"status": "success"}, nil

Consistent Error Format

// Centralized error formatter
func formatError(err error) {
    fmt.Fprintf(os.Stderr, "❌ Error: %v\n", err)

    // Add hints for common errors
    if strings.Contains(err.Error(), "connection") {
        fmt.Fprintf(os.Stderr, "Hint: Check network connectivity\n")
    }

    os.Exit(1)
}

// Use in all formatters
agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
    if err != nil {
        formatError(err)
        return
    }
    // ...
})

Respect Output Flag

When --output flag is used, respect it even with custom formatters:

// SDK handles this automatically
// Default formatters respect --output flag
// Custom formatters bypass --output flag

Next Steps