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)whereapprovedis automatically set totrue. - 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:
request.goal: The candidateGoalentity selected by the ranking algorithm (defined inembabel-agent-api/src/main/kotlin/com/embabel/agent/core/Goal.kt).request.intent: The raw user intent string that triggered goal selection.request.rankings: ARankings<Goal>object containing scored alternatives (seeembabel-agent-common/embabel-agent-ranking/src/main/kotlin/com/embabel/agent/ranking/Rankings.kt).
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-apithat controls goal execution approval. - Implementations receive a
GoalChoiceApprovalRequestcontaining 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
approveWithScoreOverfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →