Class DefaultWorkflowRuntime

java.lang.Object
com.agentforge4j.runtime.DefaultWorkflowRuntime
All Implemented Interfaces:
WorkflowRuntime

public final class DefaultWorkflowRuntime extends Object implements WorkflowRuntime
Default WorkflowRuntime implementation.

Owns the outer drive loop: resolves the workflow, creates or retrieves a WorkflowState, delegates body execution to StepSequenceExecutor, reacts to pauses and completions by updating the state and emitting events, and persists state through the injected repository after each drive.

Not thread-safe per run — a single run is driven by the caller's thread at any given time. The embedding application is responsible for preventing concurrent drives of the same run.

Construction is package-private and goes through WorkflowRuntimeBuilder. The constructors take internal collaborators (StepSequenceExecutor, ExecutableExecutor) from the non-exported com.agentforge4j.runtime.execution package, so they must not be part of the exported public API.

  • Field Details

    • DEFAULT_MAX_NESTING_DEPTH

      public static final int DEFAULT_MAX_NESTING_DEPTH
      Default maximum workflow nesting depth — guards against pathological configs.
      See Also:
  • Method Details

    • start

      public String start(String workflowId)
      Start a new run of the given workflow.
      Specified by:
      start in interface WorkflowRuntime
      Parameters:
      workflowId - id of a workflow known to the WorkflowRepository
      Returns:
      the newly-created run id
      Throws:
      IllegalArgumentException - if workflowId is blank
    • continueRun

      public void continueRun(String runId, String actorId)
      Advance a paused run. Valid when the run is in PAUSED status.
      Specified by:
      continueRun in interface WorkflowRuntime
      Parameters:
      runId - id of the run to advance
      actorId - Opaque identifier supplied by the embedding application representing the entity responsible for the action. AgentForge4j treats the value as opaque and does not interpret its structure or meaning.
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if the run is cancelled, not WorkflowStatus.PAUSED, or runId is blank
    • retry

      public void retry(String runId, String stepId, String actorId)
      Retry the given step on the given run. Honours the step's RetryPolicy: an AGENT or SPAR target is retried only when its policy's allowRetry is true and the number of attempts already made against that step's shared attempt budget — shared between this verb and any RETRY_PREVIOUS step targeting the same step — is still under the policy's maxAttempts ceiling; a granted retry consumes one unit of that budget. An AGENT/SPAR step that declares no policy defaults to RetryPolicy.none() and is therefore not retryable at all through this verb (fail-closed). A step type with no RetryPolicy concept (anything other than AGENT/SPAR) is unrestricted. A rejected retry throws IllegalStateException and leaves the run untouched.

      stepId must name a top-level step — one that appears directly in the workflow's top-level sequence. The run is repositioned at that step and the sequence is re-driven, so the target and every step after it execute again and the run finalises on the real downstream outcome (it may complete, pause, or fail). A step that exists only nested inside a blueprint or sub-workflow is rejected; retry its enclosing top-level step instead.

      Specified by:
      retry in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      stepId - id of the top-level step to retry
      actorId - Opaque identifier supplied by the embedding application representing the entity responsible for the action. AgentForge4j treats the value as opaque and does not interpret its structure or meaning.
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if stepId is blank, the run is cancelled, status is not WorkflowStatus.FAILED or WorkflowStatus.PAUSED, stepId does not identify a top-level step in the workflow definition, or runId is blank
      IllegalStateException - if the target's RetryPolicy forbids operator retry (allowRetry=false — including the undeclared-policy RetryPolicy.none() default on an AGENT/SPAR step), or the shared maxAttempts ceiling (shared with RETRY_PREVIOUS steps targeting the same step) is already reached; the run is left untouched
      Implementation Requirements:
      retry is a top-level-step contract: stepId must name a step that appears directly in the workflow's top-level sequence. The run is repositioned at that step and the enclosing sequence is re-driven, so the target and every downstream step execute again and the run finalises on the real downstream outcome. A step that exists only nested inside a blueprint or sub-workflow is rejected (retry its enclosing top-level step instead).
    • approve

      public void approve(String runId, String stepId, String approverNote, String actorId)
      Provide human approval for a step that requires it.
      Specified by:
      approve in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      stepId - id of the approved step
      approverNote - human-readable note recorded in the event log
      actorId - Opaque identifier supplied by the embedding application representing the entity responsible for the action. AgentForge4j treats the value as opaque and does not interpret its structure or meaning.
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if stepId is blank, the run is cancelled, status is not WorkflowStatus.AWAITING_APPROVAL, or runId is blank
      IllegalStateException - if stepId does not identify the step the run is suspended on
    • submitInput

      public void submitInput(String runId, Map<String,String> answers, String actorId)
      Submit answers to the pending artifact on a run in AWAITING_INPUT status.

      Keys are artifact item ids; values are the raw answers. The runtime writes each answer to the shared context under the namespaced key artifactId.itemId. Additionally, when an answer's item id is one of the current INPUT step's declared output keys, the value is also written under that bare key, so downstream steps and branches that read the declared output key resolve it.

      Specified by:
      submitInput in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      answers - map of artifact item id to user-provided answer
      actorId - Opaque identifier supplied by the embedding application representing the entity responsible for the action. AgentForge4j treats the value as opaque and does not interpret its structure or meaning.
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if answers is null, the run is cancelled, status is not WorkflowStatus.AWAITING_INPUT, a pending artifact path is inconsistent with state, or runId is blank
    • getState

      public WorkflowState getState(String runId)
      Specified by:
      getState in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      Returns:
      a defensive snapshot of the current state — mutating the returned object does not alter persisted runtime state
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if runId is blank
    • continueAfterToolApproval

      public WorkflowState continueAfterToolApproval(String runId, String toolInvocationId, ApprovalDecision decision)
      Resume a run suspended awaiting approval of a tool invocation, applying the human decision.

      Distinct from WorkflowRuntime.approve(String, String, String, String) (which resumes a step and may re-invoke the agent): this delegates to the tool-execution service's resume path — re-resolving and invoking the exact approved tool without re-invoking the LLM — and applies the outcome to state. An approved invocation that executes successfully, or a rejected one, advances past the requesting step. An approved invocation whose call fails (resolution, validation, or the provider itself) does not advance: a fresh pending row (origin EXECUTION_FAILED) is persisted and the run re-suspends in WorkflowStatus.AWAITING_TOOL_DECISION, giving the operator a further decision point via WorkflowRuntime.resolveToolDecision(java.lang.String, java.lang.String, com.agentforge4j.core.spi.tool.ToolDecision). Valid only when the run is in WorkflowStatus.AWAITING_TOOL_APPROVAL.

      An ApprovalDecision.Approve whose resumed call succeeds applies the tool result and advances the requesting step; an ApprovalDecision.Reject records the tool error and likewise advances. An approved call that fails (resolution, validation, or the provider itself) does not advance: the tool-execution service has already persisted a fresh pending row (origin EXECUTION_FAILED), and this method re-suspends the run in WorkflowStatus.AWAITING_TOOL_DECISION so the operator gets that further decision point via resolveToolDecision instead of the row being orphaned against an advanced run.

      Specified by:
      continueAfterToolApproval in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      toolInvocationId - id of the pending tool invocation being decided
      decision - the human approve/reject decision
      Returns:
      a snapshot of the resumed run state
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalStateException - if no tool-execution service is configured, or the run is in WorkflowStatus.AWAITING_APPROVAL (use approve) or WorkflowStatus.AWAITING_TOOL_DECISION (use resolveToolDecision)
      IllegalArgumentException - if the run is cancelled, not WorkflowStatus.AWAITING_TOOL_APPROVAL, or an id/argument is blank or null
      PolicyDenialTerminalException - if decision is an ApprovalDecision.Approve against a policy-denied pending invocation (a denial is terminal; see ToolExecutionService.resume)
      ToolInvocationClaimLostException - if there is no claimable pending invocation for this id — whether because a concurrent resolution already claimed and resolved it before this call's own peek, or because a concurrent resume claimed or replaced it between this call's peek and its own claim attempt; no run state is mutated when this is thrown
    • resolveToolDecision

      public WorkflowState resolveToolDecision(String runId, String toolInvocationId, ToolDecision decision)
      Resolve a run suspended in WorkflowStatus.AWAITING_TOOL_DECISION after a tool invocation was denied by policy or failed after retries, applying the operator's decision.

      ToolDecision.Continue proceeds without the tool result, writing tool.<capability>.error to context and advancing past the requesting step. ToolDecision.Retry replays the exact stored call without re-invoking the LLM: a successful replay applies the tool result and advances the requesting step, but a replay that fails again does not advance — a fresh pending row (origin EXECUTION_FAILED) is persisted and the run re-suspends in AWAITING_TOOL_DECISION for a further decision. Retry against a pending invocation that was denied by policy is invalid: a denial is terminal for that invocation, so the call throws without invoking the provider or mutating run state, leaving the pending row intact for a later ToolDecision.Continue. Valid only when the run is in AWAITING_TOOL_DECISION; use WorkflowRuntime.approve(String, String, String, String) for escalation approvals instead.

      ToolDecision.Retry against a pending invocation that a ToolPolicy denied is an invalid, non-executing transition: a policy denial is terminal for that invocation, so the underlying resume propagates a PolicyDenialTerminalException without invoking the provider or mutating run state, leaving the run suspended in WorkflowStatus.AWAITING_TOOL_DECISION with its pending row intact for a later ToolDecision.Continue. A retried call that fails again re-suspends the run in WorkflowStatus.AWAITING_TOOL_DECISION for a further decision, rather than silently advancing without a tool result.

      Specified by:
      resolveToolDecision in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      toolInvocationId - id of the pending tool invocation being resolved
      decision - the operator continue/retry decision
      Returns:
      a snapshot of the resumed run state
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalStateException - if no tool-execution service is configured, or the run is in WorkflowStatus.AWAITING_APPROVAL (use approve instead)
      IllegalArgumentException - if the run is cancelled, not WorkflowStatus.AWAITING_TOOL_DECISION, or an id is blank or the decision null
      PolicyDenialTerminalException - if decision is a ToolDecision.Retry against a policy-denied pending invocation
      ToolInvocationClaimLostException - if there is no claimable pending invocation for this id — whether because it was already claimed and resolved by a concurrent resolution before this call's own peek, or because a concurrent resolution claimed or replaced it between this call's peek and its own claim attempt. For ToolDecision.Retry the latter is detected inside toolExecutionService.resume; for ToolDecision.Continue this method claims the row itself before applying anything. No run state is mutated when this is thrown
    • cancel

      public void cancel(String runId, String actorId)
      Cancel the given run.
      Specified by:
      cancel in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      actorId - Opaque identifier supplied by the embedding application representing the entity responsible for the action. AgentForge4j treats the value as opaque and does not interpret its structure or meaning.
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if status is WorkflowStatus.COMPLETED or WorkflowStatus.FAILED, or runId is blank
    • submitReview

      public void submitReview(String runId, String stepId, String reviewNote, String actorId)
      Submit a forward-only review for a step suspended in WorkflowStatus.AWAITING_REVIEW by a HUMAN_REVIEW gate. Records the note and advances the run; this is not an approve/reject.
      Specified by:
      submitReview in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      stepId - id of the reviewed step
      reviewNote - human-readable note recorded in the event log; may be blank
      actorId - Opaque identifier supplied by the embedding application representing the entity responsible for the action. AgentForge4j treats the value as opaque and does not interpret its structure or meaning.
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if stepId/actorId is blank, the run is cancelled, or the run is not in AWAITING_REVIEW
      IllegalStateException - if the run is in a different suspension status
    • decideStepApproval

      public void decideStepApproval(String runId, String stepId, StepApprovalDecision decision)
      Decide a step suspended in WorkflowStatus.AWAITING_STEP_APPROVAL by a HUMAN_APPROVAL gate. StepApprovalDecision.Approve advances the run; StepApprovalDecision.Reject fails it (no send-back). The actor is carried inside the decision.
      Specified by:
      decideStepApproval in interface WorkflowRuntime
      Parameters:
      runId - id of the run
      stepId - id of the step being decided
      decision - the approve/reject decision
      Throws:
      ExecutionNotFoundException - if no state exists for runId
      IllegalArgumentException - if stepId is blank, decision is null, the run is cancelled, or the run is not in AWAITING_STEP_APPROVAL
      IllegalStateException - if the run is in a different suspension status