Invoking Embabel Agents
While many examples show Embabel agents being invoked via UserInput through the Embabel shell, they can also be invoked programmatically with strong typing.
This is usually how they’re used in web applications. It is also the most deterministic approach as code, rather than LLM assessment of user input, determines which agent is invoked and how.
Creating an AgentProcess Programmatically
Section titled “Creating an AgentProcess Programmatically”You can create and execute agent processes directly using the AgentPlatform:
// Create an agent process with bindingsAgentProcess agentProcess = agentPlatform.createAgentProcess( myAgent, new ProcessOptions(), Map.of("input", userRequest));
// Start the process and wait for completionObject result = agentPlatform.start(agentProcess).get();
// Or run synchronouslyAgentProcess completedProcess = agentProcess.run();MyResultType result = completedProcess.last(MyResultType.class);// Create an agent process with bindingsval agentProcess = agentPlatform.createAgentProcess( agent = myAgent, processOptions = ProcessOptions(), bindings = mapOf("input" to userRequest))
// Start the process and wait for completionval result = agentPlatform.start(agentProcess).get()
// Or run synchronouslyval completedProcess = agentProcess.run()val result = completedProcess.last<MyResultType>()You can create processes and populate their input map from varargs objects:
// Create process from objects (like in web controllers)AgentProcess agentProcess = agentPlatform.createAgentProcessFrom( travelAgent, new ProcessOptions(), travelRequest, userPreferences);// Create process from objects (like in web controllers)val agentProcess = agentPlatform.createAgentProcessFrom( agent = travelAgent, processOptions = ProcessOptions(), travelRequest, userPreferences)Using AgentInvocation
Section titled “Using AgentInvocation”AgentInvocation provides a higher-level, type-safe API for invoking agents.
It automatically finds the appropriate agent based on the expected result type.
Basic Usage
Section titled “Basic Usage”// Simple invocation with explicit result typevar invocation = AgentInvocation.create(agentPlatform, TravelPlan.class);
TravelPlan plan = invocation.invoke(travelRequest);// Type-safe invocation with inferred result typeval invocation: AgentInvocation<TravelPlan> = AgentInvocation.create(agentPlatform)
val plan = invocation.invoke(travelRequest)Invocation with Named Inputs
Section titled “Invocation with Named Inputs”// Invoke with a map of named inputsMap<String, Object> inputs = Map.of( "request", travelRequest, "preferences", userPreferences);
TravelPlan plan = invocation.invoke(inputs);// Invoke with a map of named inputsval inputs = mapOf( "request" to travelRequest, "preferences" to userPreferences)
val plan = invocation.invoke(inputs)Custom Process Options
Section titled “Custom Process Options”Configure verbosity, budget, and other execution options:
var processOptions = new ProcessOptions() .withVerbosity(new Verbosity() .withShowPrompts(true) .withShowLlmResponses(true) .withDebug(true));
var invocation = AgentInvocation.builder(agentPlatform) .options(processOptions) .build(TravelPlan.class);
TravelPlan plan = invocation.invoke(travelRequest);val processOptions = ProcessOptions( verbosity = Verbosity( showPrompts = true, showLlmResponses = true, debug = true ))
val invocation: AgentInvocation<TravelPlan> = AgentInvocation.builder(agentPlatform) .options(processOptions) .build()
val plan = invocation.invoke(travelRequest)Passing Tool Call Context at Invocation Time
Section titled “Passing Tool Call Context at Invocation Time”Use ProcessOptions.withToolCallContext() to attach out-of-band metadata that flows through the entire agent run to every tool invoked — including remote MCP tools, where it becomes MCP _meta on the wire.
This is the right place for cross-cutting infrastructure concerns such as auth tokens, tenant IDs, and correlation IDs that come from the incoming request.
// In a REST controller or event handlervar processOptions = new ProcessOptions() .withToolCallContext(Map.of( "authToken", request.getHeader("Authorization"), "tenantId", request.getHeader("X-Tenant-Id"), "correlationId", UUID.randomUUID().toString() ));
var invocation = AgentInvocation.builder(agentPlatform) .options(processOptions) .build(CustomerReport.class);
CustomerReport report = invocation.invoke(customerQuery);// In a REST controller or event handlerval processOptions = ProcessOptions() .withToolCallContext(ToolCallContext.of( "authToken" to request.getHeader("Authorization"), "tenantId" to request.getHeader("X-Tenant-Id"), "correlationId" to UUID.randomUUID().toString(), ))
val invocation = AgentInvocation.builder(agentPlatform) .options(processOptions) .build<CustomerReport>()
val report = invocation.invoke(customerQuery)Context set here can be read by any @LlmTool method that declares a ToolCallContext parameter.
It can also be supplemented per-interaction inside @Action methods using PromptRunner.withToolCallContext(); interaction-level values win on conflict.
See Receiving Out-of-Band Context in Tools for the full context pipeline.
Asynchronous Invocation
Section titled “Asynchronous Invocation”For long-running operations, use async invocation:
CompletableFuture<TravelPlan> future = invocation.invokeAsync(travelRequest);
// Handle result when completefuture.thenAccept(plan -> { logger.info("Travel plan generated: {}", plan);});
// Or wait for completionTravelPlan plan = future.get();val future: CompletableFuture<TravelPlan> = invocation.invokeAsync(travelRequest)
// Handle result when completefuture.thenAccept { plan -> logger.info("Travel plan generated: {}", plan)}
// Or wait for completionval plan = future.get()Agent Selection
Section titled “Agent Selection”AgentInvocation automatically finds agents by examining their goals:
- Searches all registered agents in the platform
- Finds agents with goals that produce the requested result type
- Uses the first matching agent found
- Throws an error if no suitable agent is available
Real-World Web Application Example
Section titled “Real-World Web Application Example”Here’s how AgentInvocation is used in the Tripper travel planning application with htmx for asynchronous UI updates:
@Controllerpublic class TripPlanningController {
private final AgentPlatform agentPlatform; private final ConcurrentHashMap<String, CompletableFuture<TripPlan>> activeJobs = new ConcurrentHashMap<>(); private static final Logger logger = LoggerFactory.getLogger(TripPlanningController.class); private static final ConcurrentHashMap<String, TripPlan> tripResultCache = new ConcurrentHashMap<>();
public TripPlanningController(AgentPlatform agentPlatform) { this.agentPlatform = agentPlatform; }
@PostMapping("/plan-trip") public String planTrip( @ModelAttribute TripRequest tripRequest, Model model) { // Generate unique job ID for tracking String jobId = UUID.randomUUID().toString();
// Create agent invocation with custom options var processOptions = new ProcessOptions() .withVerbosity(new Verbosity().withShowPrompts(true)); var invocation = AgentInvocation.builder(agentPlatform) .options(processOptions) .build(TripPlan.class);
// Start async agent execution CompletableFuture<TripPlan> future = invocation.invokeAsync(tripRequest); activeJobs.put(jobId, future);
// Set up completion handler future.whenComplete((result, throwable) -> { if (throwable != null) { logger.error("Trip planning failed for job {}", jobId, throwable); } else { logger.info("Trip planning completed for job {}", jobId); } });
model.addAttribute("jobId", jobId); model.addAttribute("tripRequest", tripRequest);
// Return htmx template that will poll for results return "trip-planning-progress"; }
@GetMapping("/trip-status/{jobId}") @ResponseBody public ResponseEntity<Map<String, Object>> getTripStatus(@PathVariable String jobId) { CompletableFuture<TripPlan> future = activeJobs.get(jobId); if (future == null) { return ResponseEntity.notFound().build(); }
if (future.isDone()) { try { TripPlan tripPlan = future.get(); activeJobs.remove(jobId);
return ResponseEntity.ok(Map.of( "status", "completed", "result", tripPlan, "redirect", "/trip-result/" + jobId )); } catch (Exception e) { activeJobs.remove(jobId); return ResponseEntity.ok(Map.of( "status", "failed", "error", e.getMessage() )); } } else if (future.isCancelled()) { activeJobs.remove(jobId); return ResponseEntity.ok(Map.of("status", "cancelled")); } else { return ResponseEntity.ok(Map.of( "status", "in_progress", "message", "Planning your amazing trip..." )); } }
@GetMapping("/trip-result/{jobId}") public String showTripResult( @PathVariable String jobId, Model model) { // Retrieve completed result from cache or database TripPlan tripPlan = tripResultCache.get(jobId); if (tripPlan == null) { return "redirect:/error"; }
model.addAttribute("tripPlan", tripPlan); return "trip-result"; }
@DeleteMapping("/cancel-trip/{jobId}") @ResponseBody public ResponseEntity<Map<String, String>> cancelTrip(@PathVariable String jobId) { CompletableFuture<TripPlan> future = activeJobs.get(jobId);
if (future != null && !future.isDone()) { future.cancel(true); activeJobs.remove(jobId); return ResponseEntity.ok(Map.of("status", "cancelled")); } else { return ResponseEntity.badRequest() .body(Map.of("error", "Job not found or already completed")); } }}@Controllerclass TripPlanningController( private val agentPlatform: AgentPlatform) {
private val activeJobs = ConcurrentHashMap<String, CompletableFuture<TripPlan>>()
@PostMapping("/plan-trip") fun planTrip( @ModelAttribute tripRequest: TripRequest, model: Model ): String { // Generate unique job ID for tracking val jobId = UUID.randomUUID().toString()
// Create agent invocation with custom options val processOptions = ProcessOptions( verbosity = Verbosity(showPrompts = true) ) val invocation: AgentInvocation<TripPlan> = AgentInvocation.builder(agentPlatform) .options(processOptions) .build()
// Start async agent execution val future = invocation.invokeAsync(tripRequest) activeJobs[jobId] = future
// Set up completion handler future.whenComplete { result, throwable -> if (throwable != null) { logger.error("Trip planning failed for job $jobId", throwable) } else { logger.info("Trip planning completed for job $jobId") } }
model.addAttribute("jobId", jobId) model.addAttribute("tripRequest", tripRequest)
// Return htmx template that will poll for results return "trip-planning-progress" }
@GetMapping("/trip-status/{jobId}") @ResponseBody fun getTripStatus(@PathVariable jobId: String): ResponseEntity<Map<String, Any>> { val future = activeJobs[jobId] ?: return ResponseEntity.notFound().build()
return when { future.isDone -> { try { val tripPlan = future.get() activeJobs.remove(jobId)
ResponseEntity.ok(mapOf( "status" to "completed", "result" to tripPlan, "redirect" to "/trip-result/$jobId" )) } catch (e: Exception) { activeJobs.remove(jobId) ResponseEntity.ok(mapOf( "status" to "failed", "error" to e.message )) } } future.isCancelled -> { activeJobs.remove(jobId) ResponseEntity.ok(mapOf("status" to "cancelled")) } else -> { ResponseEntity.ok(mapOf( "status" to "in_progress", "message" to "Planning your amazing trip..." )) } } }
@GetMapping("/trip-result/{jobId}") fun showTripResult( @PathVariable jobId: String, model: Model ): String { // Retrieve completed result from cache or database val tripPlan = tripResultCache[jobId] ?: return "redirect:/error"
model.addAttribute("tripPlan", tripPlan) return "trip-result" }
@DeleteMapping("/cancel-trip/{jobId}") @ResponseBody fun cancelTrip(@PathVariable jobId: String): ResponseEntity<Map<String, String>> { val future = activeJobs[jobId]
return if (future != null && !future.isDone) { future.cancel(true) activeJobs.remove(jobId) ResponseEntity.ok(mapOf("status" to "cancelled")) } else { ResponseEntity.badRequest() .body(mapOf("error" to "Job not found or already completed")) } }
companion object { private val logger = LoggerFactory.getLogger(TripPlanningController::class.java) private val tripResultCache = ConcurrentHashMap<String, TripPlan>() }}Key Patterns:
- Async Execution: Uses
invokeAsync()to avoid blocking the web request - Job Tracking: Maintains a map of active futures for status polling
- htmx Integration: Returns status updates that htmx can consume for UI updates
- Error Handling: Proper exception handling and user feedback
- Resource Cleanup: Removes completed jobs from memory
- Process Options: Configures verbosity and debugging for production use
Alternative: Direct AgentProcess Creation
Section titled “Alternative: Direct AgentProcess Creation”For simpler use cases, you can create and start an AgentProcess directly without AgentInvocation.
This approach is used in the Tripper application and works well with webhooks or form submissions where you want to:
- Start a long-running agent process
- Return immediately with a process ID
- Poll for status using the platform’s built-in controllers
@Controller@RequestMapping("/journey")public class JourneyController {
private final AgentPlatform agentPlatform;
public JourneyController(AgentPlatform agentPlatform) { this.agentPlatform = agentPlatform; }
@PostMapping("/plan") public String planJourney(@ModelAttribute JourneyPlanForm form, Model model) { // Convert form to domain objects TravelBrief travelBrief = new TravelBrief( form.getFrom(), form.getTo(), form.getDepartureDate(), form.getReturnDate(), form.getBrief() );
// Find the appropriate agent Agent agent = agentPlatform.agents().stream() .filter(a -> a.getName().toLowerCase().contains("travel")) .findFirst() .orElseThrow(() -> new IllegalStateException("No travel agent found"));
// Create the agent process with input bindings AgentProcess agentProcess = agentPlatform.createAgentProcessFrom( agent, new ProcessOptions( new Verbosity().withShowPrompts(true), Budget.DEFAULT // or custom budget ), travelBrief // Vararg inputs bound to blackboard );
// Start the process asynchronously agentPlatform.start(agentProcess);
// Add process ID to model for status polling model.addAttribute("processId", agentProcess.getId()); model.addAttribute("travelBrief", travelBrief);
// Return a view that polls /api/v1/process/{processId} for status return "processing"; }}@Controller@RequestMapping("/journey")class JourneyController( private val agentPlatform: AgentPlatform) {
@PostMapping("/plan") fun planJourney(@ModelAttribute form: JourneyPlanForm, model: Model): String { // Convert form to domain objects val travelBrief = TravelBrief( form.from, form.to, form.departureDate, form.returnDate, form.brief )
// Find the appropriate agent val agent = agentPlatform.agents() .filter { it.name.lowercase().contains("travel") } .firstOrNull() ?: throw IllegalStateException("No travel agent found")
// Create the agent process with input bindings val agentProcess = agentPlatform.createAgentProcessFrom( agent, ProcessOptions( verbosity = Verbosity(showPrompts = true), budget = Budget.DEFAULT // or custom budget ), travelBrief // Vararg inputs bound to blackboard )
// Start the process asynchronously agentPlatform.start(agentProcess)
// Add process ID to model for status polling model.addAttribute("processId", agentProcess.id) model.addAttribute("travelBrief", travelBrief)
// Return a view that polls /api/v1/process/{processId} for status return "processing" }}The platform provides built-in REST endpoints for status checking:
GET /api/v1/process/{processId}- Returns process status, result, and URLsDELETE /api/v1/process/{processId}- Terminates a running processGET /events/process/{processId}- SSE stream of process events
Each endpoint can be individually disabled via configuration (see Configuration).
Set the corresponding property to false to have the endpoint respond with HTTP 404:
embabel.agent.platform.rest.process-status-enabled=falseembabel.agent.platform.rest.process-kill-enabled=falseembabel.agent.platform.rest.process-events-enabled=falseA simple status polling controller can check completion and redirect to results:
@Controllerpublic class ProcessStatusController {
private final AgentPlatform agentPlatform;
public ProcessStatusController(AgentPlatform agentPlatform) { this.agentPlatform = agentPlatform; }
@GetMapping("/status/{processId}") public String checkStatus( @PathVariable String processId, @RequestParam String successView, @RequestParam String resultModelKey, Model model) {
AgentProcess process = agentPlatform.getAgentProcess(processId); if (process == null) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Process not found"); }
switch (process.getStatus()) { case COMPLETED: model.addAttribute(resultModelKey, process.lastResult()); return successView;
case FAILED: model.addAttribute("error", "Process failed: " + process.getFailureInfo()); return "error";
case TERMINATED: model.addAttribute("error", "Process was terminated"); return "error";
default: // Still running - return polling view model.addAttribute("processId", processId); return "processing"; } }}@Controllerclass ProcessStatusController( private val agentPlatform: AgentPlatform) {
@GetMapping("/status/{processId}") fun checkStatus( @PathVariable processId: String, @RequestParam successView: String, @RequestParam resultModelKey: String, model: Model ): String { val process = agentPlatform.getAgentProcess(processId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Process not found")
return when (process.status) { ProcessStatus.COMPLETED -> { model.addAttribute(resultModelKey, process.lastResult()) successView }
ProcessStatus.FAILED -> { model.addAttribute("error", "Process failed: ${process.failureInfo}") "error" }
ProcessStatus.TERMINATED -> { model.addAttribute("error", "Process was terminated") "error" }
else -> { // Still running - return polling view model.addAttribute("processId", processId) "processing" } } }}When to Use Each Approach:
| Approach | Best For |
| --- | --- |
| AgentInvocation.invokeAsync() | When you need a CompletableFuture for programmatic handling, chaining, or integration with reactive frameworks |
| Direct AgentProcess creation | Webhooks, form submissions, or UI flows where you poll for status via REST/SSE |
Webhook Integration Example
Section titled “Webhook Integration Example”For webhook-triggered workflows (e.g., JIRA, GitHub), the direct approach works well:
@RestController@RequestMapping("/webhook")public class WebhookController {
private final AgentPlatform agentPlatform;
public WebhookController(AgentPlatform agentPlatform) { this.agentPlatform = agentPlatform; }
@PostMapping("/jira/issue-created") public ResponseEntity<Map<String, String>> onJiraIssueCreated( @RequestBody JiraWebhookPayload payload) {
// Find agent that handles JIRA issues Agent agent = agentPlatform.agents().stream() .filter(a -> a.getName().contains("JiraIssue")) .findFirst() .orElseThrow(() -> new IllegalStateException("No JIRA agent configured"));
// Create domain object from webhook payload JiraIssue issue = new JiraIssue( payload.getIssue().getKey(), payload.getIssue().getFields().getSummary(), payload.getIssue().getFields().getDescription() );
// Create and start the agent process AgentProcess process = agentPlatform.createAgentProcessFrom( agent, ProcessOptions.DEFAULT, issue ); agentPlatform.start(process);
// Return process ID for status tracking return ResponseEntity.accepted().body(Map.of( "processId", process.getId(), "statusUrl", "/api/v1/process/" + process.getId(), "sseUrl", "/events/process/" + process.getId() )); }}@RestController@RequestMapping("/webhook")class WebhookController( private val agentPlatform: AgentPlatform) {
@PostMapping("/jira/issue-created") fun onJiraIssueCreated( @RequestBody payload: JiraWebhookPayload ): ResponseEntity<Map<String, String>> { // Find agent that handles JIRA issues val agent = agentPlatform.agents() .filter { it.name.contains("JiraIssue") } .firstOrNull() ?: throw IllegalStateException("No JIRA agent configured")
// Create domain object from webhook payload val issue = JiraIssue( payload.issue.key, payload.issue.fields.summary, payload.issue.fields.description )
// Create and start the agent process val process = agentPlatform.createAgentProcessFrom( agent, ProcessOptions.DEFAULT, issue ) agentPlatform.start(process)
// Return process ID for status tracking return ResponseEntity.accepted().body(mapOf( "processId" to process.id, "statusUrl" to "/api/v1/process/${process.id}", "sseUrl" to "/events/process/${process.id}" )) }}The webhook caller can then poll /api/v1/process/{processId} or subscribe to SSE events at /events/process/{processId} to track progress.
Dynamic Agent and Goal Selection with Autonomy
Section titled “Dynamic Agent and Goal Selection with Autonomy”The Autonomy class provides LLM-powered dynamic selection of agents and goals based on user intent.
Rather than programmatically choosing which agent to run, Autonomy uses an LLM to rank available agents or goals against the user’s input and select the best match.
This is how the Embabel Shell processes natural language commands.
Execution Modes
Section titled “Execution Modes”Autonomy supports two execution modes:
Closed Mode (chooseAndRunAgent): The LLM selects the most appropriate agent based on the user’s intent.
The selected agent runs in isolation using only its own actions and goals.
Open Mode (chooseAndAccomplishGoal): The LLM selects the most appropriate goal from all available goals across all agents.
Embabel then assembles a dynamic agent that can use any action from any agent to achieve that goal.
Closed Mode Example
Section titled “Closed Mode Example”Use closed mode when you want strict agent boundaries:
@Servicepublic class IntentHandler {
private final Autonomy autonomy;
public IntentHandler(Autonomy autonomy) { this.autonomy = autonomy; }
public AgentProcessExecution handleUserIntent(String userIntent) { // LLM ranks all agents and selects the best match return autonomy.chooseAndRunAgent( userIntent, ProcessOptions.DEFAULT ); }}@Serviceclass IntentHandler( private val autonomy: Autonomy) {
fun handleUserIntent(userIntent: String): AgentProcessExecution { // LLM ranks all agents and selects the best match return autonomy.chooseAndRunAgent( userIntent, ProcessOptions.DEFAULT ) }}Open Mode Example
Section titled “Open Mode Example”Use open mode when you want maximum flexibility in achieving goals:
@Servicepublic class GoalHandler {
private final Autonomy autonomy; private final AgentPlatform agentPlatform;
public GoalHandler(Autonomy autonomy, AgentPlatform agentPlatform) { this.autonomy = autonomy; this.agentPlatform = agentPlatform; }
public AgentProcessExecution handleUserIntent(String userIntent) { // LLM ranks all goals and selects the best match // Then assembles an agent from available actions to achieve it return autonomy.chooseAndAccomplishGoal( ProcessOptions.DEFAULT, GoalChoiceApprover.APPROVE_ALL, agentPlatform, // AgentScope containing goals and actions Map.of("userInput", new UserInput(userIntent)), new GoalSelectionOptions() ); }}@Serviceclass GoalHandler( private val autonomy: Autonomy, private val agentPlatform: AgentPlatform) {
fun handleUserIntent(userIntent: String): AgentProcessExecution { // LLM ranks all goals and selects the best match // Then assembles an agent from available actions to achieve it return autonomy.chooseAndAccomplishGoal( ProcessOptions.DEFAULT, GoalChoiceApprover.APPROVE_ALL, agentPlatform, // AgentScope containing goals and actions mapOf("userInput" to UserInput(userIntent)), GoalSelectionOptions() ) }}Using Arbitrary Bindings
Section titled “Using Arbitrary Bindings”chooseAndAccomplishGoal accepts any bindings, not just UserInput.
A BindingsFormatter extracts intent text from the bindings for goal ranking:
public AgentProcessExecution processTask(Task task, Person person) { // Bindings can be any objects Map<String, Object> bindings = Map.of( "task", task, "person", person );
return autonomy.chooseAndAccomplishGoal( ProcessOptions.DEFAULT, GoalChoiceApprover.APPROVE_ALL, agentPlatform, bindings, new GoalSelectionOptions(), BindingsFormatter.DEFAULT // Extracts intent from bindings );}fun processTask(task: Task, person: Person): AgentProcessExecution { // Bindings can be any objects val bindings = mapOf( "task" to task, "person" to person )
return autonomy.chooseAndAccomplishGoal( ProcessOptions.DEFAULT, GoalChoiceApprover.APPROVE_ALL, agentPlatform, bindings, GoalSelectionOptions(), BindingsFormatter.DEFAULT // Extracts intent from bindings )}The default BindingsFormatter extracts text using this priority:
PromptContributor.contribution()if the object implementsPromptContributorHasInfoString.infoString()if the object implementsHasInfoStringtoString()otherwise
You can provide a custom formatter:
BindingsFormatter customFormatter = bindings -> { Task task = (Task) bindings.get("task"); Person person = (Person) bindings.get("person"); return String.format("Process task '%s' for %s", task.getDescription(), person.getName());};
return autonomy.chooseAndAccomplishGoal( ProcessOptions.DEFAULT, GoalChoiceApprover.APPROVE_ALL, agentPlatform, bindings, new GoalSelectionOptions(), customFormatter);val customFormatter = BindingsFormatter { bindings -> val task = bindings["task"] as Task val person = bindings["person"] as Person "Process task '${task.description}' for ${person.name}"}
return autonomy.chooseAndAccomplishGoal( ProcessOptions.DEFAULT, GoalChoiceApprover.APPROVE_ALL, agentPlatform, bindings, GoalSelectionOptions(), customFormatter)Goal Choice Approval
Section titled “Goal Choice Approval”You can require approval before executing a selected goal:
// Approve only high-confidence matchesGoalChoiceApprover approver = GoalChoiceApprover.approveWithScoreOver(0.8);
// Or implement custom approval logicGoalChoiceApprover customApprover = request -> { if (request.getGoal().getName().contains("dangerous")) { return new GoalChoiceNotApproved("Dangerous goals require manual approval"); } return GoalChoiceApproved.INSTANCE;};// Approve only high-confidence matchesval approver = GoalChoiceApprover.approveWithScoreOver(0.8)
// Or implement custom approval logicval customApprover = GoalChoiceApprover { request -> if (request.goal.name.contains("dangerous")) { GoalChoiceNotApproved("Dangerous goals require manual approval") } else { GoalChoiceApproved }}Confidence Thresholds
Section titled “Confidence Thresholds”Autonomy uses configurable confidence thresholds to filter matches.
If no agent or goal exceeds the threshold, a NoAgentFound or NoGoalFound exception is thrown.
Configure thresholds in application.properties:
# Minimum confidence for agent selection (0.0 to 1.0)embabel.agent.platform.autonomy.agent-confidence-cut-off=0.6
# Minimum confidence for goal selection (0.0 to 1.0)embabel.agent.platform.autonomy.goal-confidence-cut-off=0.6Or override per-request using GoalSelectionOptions:
GoalSelectionOptions options = new GoalSelectionOptions( 0.5, // goalConfidenceCutOff - override platform default null, // agentConfidenceCutOff - use platform default false // multiGoal - whether to select multiple goals);val options = GoalSelectionOptions( goalConfidenceCutOff = 0.5, // override platform default agentConfidenceCutOff = null, // use platform default multiGoal = false // whether to select multiple goals)Shell Usage
Section titled “Shell Usage”The Embabel Shell uses Autonomy for the execute (x) and choose-goal commands:
# Closed mode (default) - select best agentx "Find a horoscope for Alice who is a Scorpio"
# Open mode - select best goal, use any actionsx "Find a horoscope for Alice who is a Scorpio" -o
# Show goal rankings without executingchoose-goal "Find a horoscope for Alice"See execute (x) and Shell Commands for full command and flag documentation.
Handling Selection Failures
Section titled “Handling Selection Failures”try { return autonomy.chooseAndRunAgent(userIntent, ProcessOptions.DEFAULT);} catch (NoAgentFound e) { // No agent matched with sufficient confidence logger.info("No matching agent. Rankings: {}", e.getAgentRankings()); return fallbackResponse();} catch (NoGoalFound e) { // No goal matched with sufficient confidence (open mode) logger.info("No matching goal. Rankings: {}", e.getGoalRankings()); return fallbackResponse();} catch (GoalNotApproved e) { // Goal was rejected by the approver logger.info("Goal not approved: {}", e.getReason()); return requiresApprovalResponse();}try { return autonomy.chooseAndRunAgent(userIntent, ProcessOptions.DEFAULT)} catch (e: NoAgentFound) { // No agent matched with sufficient confidence logger.info("No matching agent. Rankings: {}", e.agentRankings) return fallbackResponse()} catch (e: NoGoalFound) { // No goal matched with sufficient confidence (open mode) logger.info("No matching goal. Rankings: {}", e.goalRankings) return fallbackResponse()} catch (e: GoalNotApproved) { // Goal was rejected by the approver logger.info("Goal not approved: {}", e.reason) return requiresApprovalResponse()}