CLI-Enabled Reasoners
Expose reasoners as CLI commands with WithCLI() options
Overview
Reasoners become CLI commands when registered with CLI options. The same reasoner can serve both API requests (server mode) and CLI invocations (CLI mode).
CLI Reasoner Options
WithCLI()
Enable CLI access for this reasoner, making it available as a command.
agent.RegisterReasoner("greet", handler, agent.WithCLI())Usage:
# Explicit command name
./binary greet --set name=AliceWithDefaultCLI()
Set as default command - users can omit the command name. Must be used with WithCLI().
agent.RegisterReasoner("greet", handler,
agent.WithCLI(),
agent.WithDefaultCLI(),
)Usage:
# Default command (no "greet" needed)
./binary --set name=Alice
# Can still use explicit name
./binary greet --set name=AliceOnly one reasoner can be marked with WithDefaultCLI(). The last one registered wins.
Input Methods
CLI mode supports four input methods, merged with priority order:
1. Individual Flags (--set key=value)
Pass parameters as individual flags. Can be repeated.
./binary greet --set name=Alice --set greeting="Hello" --set age=30Maps to:
{
"name": "Alice",
"greeting": "Hello",
"age": "30"
}Values are strings by default. Your reasoner handler should parse types as needed.
2. JSON String (--input)
Pass entire input as JSON string.
./binary greet --input '{"name":"Alice","age":30}'3. JSON File (--input-file)
Load input from a JSON file.
# data.json
{
"name": "Alice",
"age": 30
}
# Usage
./binary greet --input-file data.json4. Stdin (Implicit)
Pipe JSON data to stdin.
echo '{"name":"Alice"}' | ./binary greet
# Or from file
cat data.json | ./binary greetMerging Priority
When multiple input methods are used, they merge with this priority:
--set > --input > --input-file > stdinExample:
# base.json: {"name":"Alice","age":30}
./binary --input-file base.json --set name=Bob --input '{"greeting":"Hi"}'
# Result: {"name":"Bob","age":30,"greeting":"Hi"}
# "Bob" from --set overrides "Alice" from fileMultiple CLI Reasoners
Register multiple reasoners as CLI commands:
a.RegisterReasoner("greet", greetHandler,
agent.WithCLI(),
agent.WithDefaultCLI(), // Default command
)
a.RegisterReasoner("analyze", analyzeHandler,
agent.WithCLI(),
agent.WithDescription("Analyze text with AI"),
)
a.RegisterReasoner("summarize", summarizeHandler,
agent.WithCLI(),
agent.WithDescription("Summarize documents"),
)Usage:
# Default command (greet)
./binary --set name=Alice
# Explicit commands
./binary greet --set name=Bob
./binary analyze --input-file doc.txt
./binary summarize --set url=https://example.com/articleList available commands:
./binary --helpCLI-Only vs Server-Accessible
Not all reasoners need CLI access. You can have:
- CLI-only reasoners: Registered with
WithCLI(), not called by other agents - Server-only reasoners: No
WithCLI(), only accessible via control plane - Dual-access reasoners: With
WithCLI(), accessible both ways
// CLI-only (not exposed to control plane routing)
a.RegisterReasoner("local-debug", handler, agent.WithCLI())
// Server-only (internal reasoner, no CLI)
a.RegisterReasoner("internal-process", handler)
// Dual-access (both CLI and server)
a.RegisterReasoner("analyze", handler, agent.WithCLI())CLI Context Detection
Reasoners can detect if they're running in CLI mode:
a.RegisterReasoner("greet", func(ctx context.Context, input map[string]any) (any, error) {
name := input["name"].(string)
if agent.IsCLIMode(ctx) {
// CLI mode: return simple string
return fmt.Sprintf("Hello, %s!", name), nil
}
// Server mode: return structured data
return map[string]any{
"greeting": fmt.Sprintf("Hello, %s!", name),
"timestamp": time.Now().Unix(),
}, nil
}, agent.WithCLI())Command Naming
Command names are derived from reasoner names:
| Reasoner Name | CLI Command |
|---|---|
"greet" | ./binary greet |
"analyze-text" | ./binary analyze-text |
"summarize_doc" | ./binary summarize_doc |
Best practices:
- Use lowercase names
- Use hyphens for multi-word commands (
analyze-textnotanalyzeText) - Keep names short and descriptive
Built-in Commands
These commands are always available:
| Command | Description |
|---|---|
serve | Start server mode (connect to control plane) |
--help | Show help information |
--version | Show version information |
You cannot override these commands with custom reasoners.
Examples
Single Default Command
a.RegisterReasoner("process", func(ctx context.Context, input map[string]any) (any, error) {
// Process input
return result, nil
},
agent.WithCLI(),
agent.WithDefaultCLI(),
agent.WithDescription("Process input data"),
)./tool --set input="data"
./tool process --input-file data.json # Also worksMultiple Commands
a.RegisterReasoner("greet", greetHandler,
agent.WithCLI(),
agent.WithDefaultCLI(),
)
a.RegisterReasoner("farewell", farewellHandler,
agent.WithCLI(),
)
a.RegisterReasoner("analyze", analyzeHandler,
agent.WithCLI(),
)./tool --set name=Alice # Uses default (greet)
./tool greet --set name=Bob # Explicit greet
./tool farewell --set name=Charlie # Different command
./tool analyze --input-file doc.txt # Another commandMixed Access Modes
// Public CLI command
a.RegisterReasoner("search", searchHandler,
agent.WithCLI(),
agent.WithDescription("Search documents"),
)
// Internal server-only reasoner
a.RegisterReasoner("index-documents", indexHandler)
// Both CLI and server
a.RegisterReasoner("analyze", analyzeHandler,
agent.WithCLI(),
)Advanced Input Parsing
Type Conversion
Values from --set are strings. Parse as needed:
a.RegisterReasoner("calculate", func(ctx context.Context, input map[string]any) (any, error) {
// Parse age to int
ageStr, ok := input["age"].(string)
if !ok {
return nil, fmt.Errorf("age required")
}
age, err := strconv.Atoi(ageStr)
if err != nil {
return nil, fmt.Errorf("age must be a number: %w", err)
}
// Use parsed value
return age * 2, nil
}, agent.WithCLI())Boolean Flags
# Boolean as string
./binary --set enabled=true --set verbose=falsefunc parseBool(input map[string]any, key string) bool {
val, ok := input[key].(string)
if !ok {
return false
}
return val == "true" || val == "1" || val == "yes"
}JSON Nested Values
Use --input or --input-file for complex structures:
./binary --input '{
"user": {"name": "Alice", "age": 30},
"options": {"verbose": true, "format": "json"}
}'