Conversation
* Introduced graceful termination event via BlackBoard * Introduced immediate termination via Termination Exception * Added Termination handling logic to abstract, simple, and concurrent agent * Added Termination handling logic to Default and Parallel tool loops
|
The termination context is currently string-only ( String-only is probably fine — adding an optional |
|
The termination signal is currently stored on the Blackboard via a magic string key ( Asking because it leads to a couple of friction points: the Blackboard doesn't support key removal ("objects are immutable and may not be removed"), so Given that |
|
The PR description documents the mechanics of signal vs exception well. The project docs use practical code examples throughout (e.g., the tools reference) — how about enhancing the docs with practical "when would I choose which" supported by examples? The key distinction is about side effects: // Signal: "Let me finish my work, then stop"
@LlmTool(description = "Save and shutdown")
fun saveAndStop(ctx: ProcessContext): String {
customerRepository.save(record) // side effect completes
ctx.terminateAction("Save complete, no more work needed")
return "Saved" // tool finishes normally
}
// Exception: "Stop now, nothing left to do"
@LlmTool(description = "Check service health")
fun checkHealth(): String {
if (!mcpClient.isConnected("required_service")) {
throw TerminateActionException("Service unavailable")
// nothing after this runs
}
return "Healthy"
}It is especially worth noting the key difference between sequential and parallel stop points. In both modes, neither mechanism can stop sibling tools already in flight — that's inherent to parallelism. For example, if Currently there's an additional difference: in Adding a |
|
Quick question on the retry behavior for terminated actions: I wrote a test to check: val PermanentlyTerminatingActionAgent = agent("PermanentTerminator", description = "Agent that always terminates action") {
transformation<UserInput, TestPerson>(name = "always_terminating_action") {
val attemptCount = (it["attemptCount"] as? Int) ?: 0
it["attemptCount"] = attemptCount + 1
throw TerminateActionException("Service permanently unavailable")
}
// ...goal and other transformations...
}
@Test
fun `permanently terminating action retries until action budget exhausted`() {
val actionBudget = 5
val agentProcess = SimpleAgentProcess(
id = "test-permanent-action-termination",
agent = PermanentlyTerminatingActionAgent,
processOptions = ProcessOptions(budget = Budget(actions = actionBudget)),
// ...
)
val result = agentProcess.run()
// The action was retried on every tick until budget exhausted
assertThat(blackboard["attemptCount"] as Int).isEqualTo(actionBudget) // all 5 burned
assertThat(result.status).isEqualTo(AgentProcessStatusCode.TERMINATED)
}This passes — the action retries all 5 times before Would it make sense to still set |
|
In
The process reports TERMINATED, and the failure from Action A is silently dropped — no log, no Is that intentional? Could be worth either logging the concurrent failure when AGENT_TERMINATED wins, or returning a richer result that surfaces both signals. What do you think? |
|
@jasperblues - thanks for extenive feedback.
|
Review feedbackItems completed:
|
|
Looks good! A couple of small things I noticed:
Also — do you think it would be worth adding some "when would I choose signal vs exception" examples to the user-facing docs? Something like the signal-for-side-effects vs exception-for-immediate-stop pattern. Totally optional, but could help users pick the right mechanism without having to read the internals. |
|
@jasperblues - agreed with comments, will address. thank you |
==> addressed both. examples for user guide will be added as per suggestion. |
poutsma
left a comment
There was a problem hiding this comment.
Looking good in general, couple of comments.
embabel-agent-api/src/main/kotlin/com/embabel/agent/api/termination/TerminationExtensions.kt
Outdated
Show resolved
Hide resolved
embabel-agent-api/src/main/kotlin/com/embabel/agent/api/tool/TerminationExceptions.kt
Show resolved
Hide resolved
- Introduced TerminationException as a parent to Action and Agent Termination Exceptions - Introduced public API terminateAgent/Action on AgentProcess interface - Updated User Guide
Summary of changes:
|
- Introduced TerminationException as a parent to Action and Agent Termination Exceptions - Introduced public API terminateAgent/Action on AgentProcess interface - Updated User Guide
|



OVERVIEW
Agent/Action Early Termination
Please refer to:
#783 and
#1481
Problem Statement
Agents and actions currently lack a mechanism to terminate gracefully or immediately based on
runtime conditions. When a tool detects an unrecoverable situation (e.g., required MCP service
unavailable, invalid state, resource exhaustion), it has no clean way to signal that the action or
entire agent should stop.
This PR introduces a termination API with two scopes (ACTION, AGENT) and two mechanisms (graceful,
immediate), giving tools and actions fine-grained control over execution flow.
Usage
Immediate Termination (Exception-based)
Use when you need to stop right now:
DETAILED COMPARISON
Important: Graceful terminateAction() only works in LLM-based actions with tool loops. For normal agent actions, use throw TerminateActionException().
Graceful vs Immediate Termination
Aspect: Mechanism
Graceful (Signal): Sets signal on blackboard
Immediate (Exception): Throws exception
────────────────────────────────────────
Aspect: When checked
Graceful (Signal): At next checkpoint
Immediate (Exception): Immediately
────────────────────────────────────────
Aspect: Current work
Graceful (Signal): Completes
Immediate (Exception): Stops
────────────────────────────────────────
Aspect: API
Graceful (Signal): ctx.terminateAgent() / ctx.terminateAction()
Immediate (Exception): throw TerminateAgentException() / throw TerminateActionException()
────────────────────────────────────────
Aspect: Use case
Graceful (Signal): Clean shutdown, allow cleanup
Immediate (Exception): Critical failure, invalid state
Tool Loop Handling Comparison
Key Implementation Details
AgentProcessStatusCode.RUNNING (agent continues).
AgentProcessStatusCode.TERMINATED (agent stops).
Files Changed
New files:
Modified: