How to Implement the GoalChoiceApprover Interface in Embabel Agent

The GoalChoiceApprover interface allows you to inject custom veto logic into the Embabel Agent goal selection process by implementing a single function that returns either GoalChoiceApproved or GoalChoiceNotApproved based on inspection of the candidate goal, user intent, and ranking scores.

The GoalChoiceApprover interface is a critical extension point in the Embabel Agent framework that governs whether a selected goal should be executed or rejected. Located in the embabel-agent-api module, this functional interface enables developers to implement safety guards, confidence thresholds, and business logic filters before the agent acts on a goal. Implementing this interface requires only a single method that evaluates a GoalChoiceApprovalRequest and returns a definitive approval response.

Understanding the GoalChoiceApprover Contract

The interface definition resides in embabel-agent-api/src/main/kotlin/com/embabel/agent/api/common/autonomy/GoalChoiceApprover.kt (lines 49-76). It is declared as a fun interface, meaning it supports lambda syntax and single-method implementations.

The contract is straightforward: given a GoalChoiceApprovalRequest containing the candidate Goal, the original user intent string, and a Rankings<Goal> object, your implementation must return a GoalChoiceApprovalResponse. This design pattern allows the agent to query external validation logic before committing to goal execution.

Core Response Types

The framework provides two concrete response implementations:

  • GoalChoiceApproved: Signals acceptance of the goal. Instantiate with GoalChoiceApproved(request) where approved is automatically set to true.
  • GoalChoiceNotApproved: Signals rejection. Instantiate with GoalChoiceNotApproved(request, "reason") to provide an explanation for the veto.

Both classes implement the GoalChoiceApprovalResponse sealed interface and are immutable data classes suitable for concurrent environments.

Implementing the Interface

Because GoalChoiceApprover is a functional interface, you can implement it using a lambda expression without creating a named class.

Basic Lambda Implementation

Create an approver instance directly:

val myApprover = GoalChoiceApprover { request ->
    // Custom validation logic
    if (isValid(request)) {
        GoalChoiceApproved(request)
    } else {
        GoalChoiceNotApproved(request, "Validation failed")
    }
}

Accessing Request Data

The GoalChoiceApprovalRequest exposes three key properties:

Inspect these fields to implement context-aware approval logic.

Score-Based Validation

To approve only when confidence exceeds a threshold, inspect the top ranking:

val topScore = request.rankings.rankings().firstOrNull()?.score ?: 0.0
if (topScore > 0.8) {
    GoalChoiceApproved(request)
} else {
    GoalChoiceNotApproved(request, "Confidence $topScore below threshold")
}

Built-in Factory Methods

The interface provides convenient factory methods for common use cases:

APPROVE_ALL: A default implementation that approves every request, defined at line 59:

val approver = GoalChoiceApprover.APPROVE_ALL

approveWithScoreOver: Creates an approver that validates against a minimum ZeroToOne score threshold (lines 62-73):

val strictApprover = GoalChoiceApprover.approveWithScoreOver(ZeroToOne.of(0.75))

Integration with Agent Configuration

Once implemented, register your approver with the agent builder. Most AgentBuilder implementations (located in embabel-agent-api/src/main/kotlin/com/embabel/agent/api/AgentBuilder.kt) accept the approver via a dedicated setter:

val agent = AgentBuilder()
    .goalChoiceApprover(myApprover)
    // ... other configuration
    .build()

The approver is invoked during the goal selection phase, after ranking but before execution.

Complete Implementation Example

Below is a production-ready implementation that combines multiple validation strategies:

val safeApprover = GoalChoiceApprover { request ->
    // Block debugging intents in production
    if ("debug" in request.intent.lowercase()) {
        GoalChoiceNotApproved(
            request,
            reason = "Debug intents are disallowed in production environments."
        )
    } else {
        // Enforce minimum confidence threshold
        val topScore = request.rankings.rankings().firstOrNull()?.score ?: 0.0
        if (topScore <= 0.5) {
            GoalChoiceNotApproved(
                request,
                reason = "Score $topScore is not above 0.5 threshold"
            )
        } else {
            GoalChoiceApproved(request)
        }
    }
}

For reusable composition, chain the built-in factory with custom logic:

val composedApprover = GoalChoiceApprover { request ->
    // First apply standard score validation
    val scoreResult = GoalChoiceApprover.approveWithScoreOver(ZeroToOne.of(0.5))(request)
    if (!scoreResult.approved) return@GoalChoiceApprover scoreResult
    
    // Then apply custom business rules
    if (request.goal.id.value.contains("restricted")) {
        GoalChoiceNotApproved(request, "Restricted goal category")
    } else {
        scoreResult
    }
}

Summary

  • GoalChoiceApprover is a functional interface in embabel-agent-api that controls goal execution approval.
  • Implementations receive a GoalChoiceApprovalRequest containing the goal, intent, and rankings.
  • Return GoalChoiceApproved to allow execution or GoalChoiceNotApproved to veto with a reason.
  • Use lambda syntax for concise implementations or factory methods like approveWithScoreOver for standard validations.
  • Register the approver via the AgentBuilder configuration to activate it in the agent pipeline.

Frequently Asked Questions

What is the difference between GoalChoiceApprover and GoalRanker?

While GoalRanker orders candidate goals by relevance, GoalChoiceApprover acts as a final gatekeeper that can veto even the top-ranked goal. The ranker determines which goal is best; the approver determines if that goal is acceptable to execute.

Can I use multiple GoalChoiceApprovers together?

The interface design supports composition through decorator patterns. Wrap multiple approvers in a single implementation that chains their logic, or create a composite that fails fast on the first rejection. The built-in APPROVE_ALL constant serves as a convenient identity element for composition.

How do I access the confidence score of the selected goal?

Inspect request.rankings.rankings().firstOrNull()?.score within your implementation. The Rankings API (defined in embabel-agent-common/embabel-agent-ranking/src/main/kotlin/com/embabel/agent/ranking/Rankings.kt) returns a sorted list where the first element represents the highest-scored goal selected by the ranker.

Is the GoalChoiceApprover thread-safe?

Yes. The interface is stateless by design, and the provided response types (GoalChoiceApproved and GoalChoiceNotApproved) are immutable data classes. Implementations should avoid maintaining mutable state to ensure safe concurrent execution across multiple agent requests.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →