- All Implemented Interfaces:
ToolExecutionService
ToolExecutionService: the single chokepoint for tool invocation. It emits audit
events, resolves the capability, validates arguments, evaluates policy, and invokes the provider
under an
authoritative hard timeout that the service owns regardless of provider behaviour. On
approval it persists a PendingToolInvocation and resumes later via resume(java.lang.String, java.lang.String, com.agentforge4j.core.spi.tool.ApprovalDecision), never
re-invoking the LLM.-
Constructor Summary
ConstructorsConstructorDescriptionDefaultToolExecutionService(ToolProviderResolver resolver, ToolPolicy policy, PendingToolInvocationStore pendingStore, ToolExecutionOptions options, EventRecorder eventRecorder, com.fasterxml.jackson.databind.ObjectMapper objectMapper, Clock clock) Creates the chokepoint service with its collaborators. -
Method Summary
Modifier and TypeMethodDescriptionExecutes a requested tool invocation.resume(String runId, String toolInvocationId, ApprovalDecision decision) Resumes a previously suspended tool invocation — awaiting approval (ToolExecutionOutcome.Status.APPROVAL_PENDING), or awaiting an operator decision after a policy denial or an execution failure — after a human decision.
-
Constructor Details
-
DefaultToolExecutionService
public DefaultToolExecutionService(ToolProviderResolver resolver, ToolPolicy policy, PendingToolInvocationStore pendingStore, ToolExecutionOptions options, EventRecorder eventRecorder, com.fasterxml.jackson.databind.ObjectMapper objectMapper, Clock clock) Creates the chokepoint service with its collaborators.- Parameters:
resolver- capability resolver (sole resolver; fail-fast)policy- policy gatependingStore- store for approval-pending invocationsoptions- invocation tunables, including the authoritative timeouteventRecorder- audit sinkobjectMapper- JSON mapper for argument validation and event payloadsclock- clock for pending-invocation timestamps
-
-
Method Details
-
execute
Description copied from interface:ToolExecutionServiceExecutes a requested tool invocation.- Specified by:
executein interfaceToolExecutionService- Parameters:
cmd- the requested invocationctx- invocation context- Returns:
- the terminal outcome
-
resume
public ToolExecutionOutcome resume(String runId, String toolInvocationId, ApprovalDecision decision) Resumes a previously suspended tool invocation — awaiting approval (ToolExecutionOutcome.Status.APPROVAL_PENDING), or awaiting an operator decision after a policy denial or an execution failure — after a human decision.For an
PendingToolInvocation.Origin.APPROVAL_REQUIREDorPendingToolInvocation.Origin.EXECUTION_FAILEDrow, the human decision supersedes the originalPolicyDecision.RequireApprovalrule: policy is not re-evaluated. The capability is re-resolved and arguments re-validated against current configuration, and the pending record is atomically claimed — tied to the exact row this call observed (seePendingToolInvocationStore.claim(java.lang.String, java.lang.String, com.agentforge4j.core.spi.tool.PendingToolInvocation)) — before invocation, so that two concurrent resumes on the same invocation never both execute. If there is no claimable pending row for this invocation — whether because it was never pending, already resolved, or claimed/replaced by a concurrent resume before this call's own claim attempt — this call throwsToolInvocationClaimLostExceptionwithout mutating any state; this is a benign concurrency-loss signal, never reported as a provider/tool failure. If the claimed attempt itself fails, a fresh pending row is persisted so the operator gets a further decision point.For a
PendingToolInvocation.Origin.POLICY_DENIEDrow the denial is terminal:ApprovalDecision.Approveis rejected with aPolicyDenialTerminalExceptionwithout invoking the provider and without consuming the pending row; onlyApprovalDecision.Rejectresolves it through this method (the runtime's non-executingToolDecision.Continueresolves it without ever callingresume— it claims the pending row directly, using the same atomic, exact-row-tied contract).If there is no claimable pending row for
runId/toolInvocationId— never pending, already resolved, or already claimed/replaced by a concurrent resume — this call throwsToolInvocationClaimLostExceptionwithout mutating any state; this is a benign concurrency-loss signal, never reported as a provider/tool failure. When a row is found, aPendingToolInvocation.Origin.POLICY_DENIEDrow is terminal: anApprovalDecision.Approve— whether issued by the runtime'sToolDecision.Retryor a direct SPI call — never reaches the provider for such a row. Before treating it as terminal, this re-verifies (viaPendingToolInvocationStore.verifyStillCurrent(java.lang.String, java.lang.String, com.agentforge4j.core.spi.tool.PendingToolInvocation)) that the exact row this call originally observed is still the current one — a concurrentApprovalDecision.Rejector the runtime's non-executingToolDecision.Continuemay have already resolved it in the meantime, in which case this call throwsToolInvocationClaimLostExceptioninstead, never an override-rejected event for a row that is no longer even pending. Only once still confirmed current is it rejected with aPolicyDenialTerminalExceptionand an audited override-rejected event, with the row left in place (never claimed/removed here) so that later legitimate resolver can still resolve it. Every other pending row is claimed atomically, tied to the exact row this call observed viaPendingToolInvocationStore.find(java.lang.String, java.lang.String)(seePendingToolInvocationStore.claim(java.lang.String, java.lang.String, com.agentforge4j.core.spi.tool.PendingToolInvocation)), before its provider is invoked, so at most one of two concurrent resumes on the same invocation executes; the other — or a resume whose observed row was replaced by a different one before its own claim attempt — never reaches the provider and throws the sameToolInvocationClaimLostException. If the claimed call itself ends inToolExecutionOutcome.Status.FAILED, a fresh pending row is persisted (originPendingToolInvocation.Origin.EXECUTION_FAILED) so the operator gets a further decision point rather than the run silently losing the pending invocation.- Specified by:
resumein interfaceToolExecutionService- Parameters:
runId- non-blank run that owns the pending invocationtoolInvocationId- non-blank pending invocation iddecision- the human decision- Returns:
- the terminal outcome
- Throws:
ToolInvocationClaimLostException- if there is no claimable pending row for this invocation, whether because it was never pending, already resolved, or claimed/replaced by a concurrent resume before this call's own claim attempt
-