Stop Conditions
Introduction
The agent loop runs iteratively — calling the model, executing tools, and repeating — until something tells it to stop. Understanding the stop condition system is essential for building predictable agents that terminate gracefully under all circumstances. Three mechanisms work together to control loop termination: stop signals emitted by guards or tools, continuation overrides that can suppress those signals, and the AgentStopException for immediate termination from within tool code.How the Loop Decides to Stop
At the end of each iteration, the loop evaluatesExecutionState::shouldStop(). The decision follows this priority chain:
- If a stop signal has been emitted and no continuation override is active, the loop stops immediately.
- If a continuation override is active, the loop continues regardless of stop signals.
- If the model returned tool calls, the loop continues to execute them.
- If none of the above apply (the model gave a final text response with no tool calls), the loop stops — this is the normal completion path.
Stop Signals
AStopSignal is an immutable value object that represents a structured request to terminate the loop. It carries a reason, a human-readable message, contextual data for debugging, and the class name of the source that created it:
| Property | Type | Description |
|---|---|---|
reason | StopReason | An enum value categorizing why the stop was requested |
message | string | A human-readable description of the stop condition |
context | array | Arbitrary diagnostic data (thresholds, counters, timestamps) for debugging and logging |
source | ?string | The fully-qualified class name of the hook or component that emitted the signal |
StopSignals collection within ExecutionContinuation. Multiple signals can coexist — for instance, both a step limit and a token limit might trigger in the same iteration. Use highest() to retrieve the most authoritative signal by priority, or first() for the earliest-added signal.
Displaying and Serializing Signals
Signals provide methods for display and persistence:Factory Methods
StopSignal provides static factories for common signal types so you don’t have to construct them manually:
Creating Signals from Exceptions
When anAgentStopException is caught by the loop, the exception is converted to a StopSignal using the dedicated factory method:
Emitting Stop Signals from Hooks
Guard hooks are the primary source of stop signals. A hook emits a signal by modifying the agent state and returning the updated context:withStopSignal() method on AgentState appends the signal to the execution’s ExecutionContinuation state. The loop checks shouldStop() after processing hooks at the end of each step.
The StopSignals Collection
Multiple stop signals can accumulate during execution. TheStopSignals collection is an immutable container that manages them:
withSignal() call returns a new instance. The collection supports full serialization through toArray() and fromArray().
StopReason
TheStopReason enum categorizes every possible reason for stopping the agent loop. Each reason has a string value for serialization and a numeric priority for comparison:
| Reason | Value | Priority | Description |
|---|---|---|---|
ErrorForbade | error | 0 (highest) | An error prevented continuation |
StopRequested | stop_requested | 1 | Explicit stop via AgentStopException |
StepsLimitReached | steps_limit | 2 | Step budget exhausted |
TokenLimitReached | token_limit | 3 | Token budget exhausted |
TimeLimitReached | time_limit | 4 | Wall-clock time budget exhausted |
RetryLimitReached | retry_limit | 5 | Maximum retries exceeded |
FinishReasonReceived | finish_reason | 6 | LLM finish reason matched a stop condition |
UserRequested | user_requested | 2 | External cancellation requested by the caller |
Completed | completed | 8 | Normal, successful completion |
Unknown | unknown | 9 (lowest) | Unclassified stop reason |
Priority and Comparison
EachStopReason has a numeric priority that determines its severity. Lower numbers indicate more urgent reasons — ErrorForbade (0) takes precedence over Completed (8). This ordering is used when evaluating multiple signals:
Distinguishing Graceful Stops from Forced Stops
ThewasForceStopped() method is particularly useful for determining how the agent finished after execution. Natural endings return false, while all resource limits, errors, and explicit stops return true:
AgentStopException
When a tool determines that the agent’s task is complete (or that execution should not continue), it can throw anAgentStopException. The loop catches this exception, converts it to a StopSignal with reason StopRequested, and terminates cleanly.
AgentStopException extends RuntimeException and is a control-flow exception — it is not an error condition, but an intentional mechanism for tools to signal completion:
| Property | Type | Description |
|---|---|---|
signal | StopSignal | The stop signal to emit when the exception is caught |
step | ?AgentStep | An optional reference to the current step for diagnostic purposes |
context | array | Additional context data passed through to StopSignal::fromStopException() |
source | ?string | The class that threw the exception, for traceability |
Common Use Cases for AgentStopException
Task completion tool — Let the model signal that it has finished its task:ExecutionContinuation
ExecutionContinuation is the state object that manages the interplay between stop signals and continuation requests. It holds two independent pieces of state:
StopSignals— the collection of accumulated stop signalsisContinuationRequested— a boolean flag that overrides stop signals whentrue
shouldStop(), which returns true only when signals exist and no continuation has been requested:
Modifying Continuation State
ExecutionContinuation is immutable. All modifications return new instances:
Overriding Stop Signals with Continuation
In some scenarios, you may want the loop to continue even after a stop signal has been emitted. For example, a summarization hook might intercept a step-limit signal, summarize the conversation to free up context space, and request continuation:withExecutionContinued() method on AgentState sets the continuation flag to true, which causes shouldStop() to return false even though stop signals are present. This gives hooks the power to implement recovery strategies before allowing the loop to terminate.
Caution: Overriding stop signals should be done carefully. If a continuation hook resets the signal but the underlying condition persists (e.g., the token limit is still exceeded after summarization), the guard hook will re-emit the signal on the next step, potentially creating an infinite loop. Always ensure the override resolves the root cause.
Diagnostic Output
Theexplain() method produces a human-readable summary of the continuation state, useful for logging and debugging:
Inspecting Stop Reasons After Execution
After the loop completes, you can inspect why it stopped through the agent state:Serialization
All stop condition components support full serialization for persistence and debugging:Combining Guards and Stop Tools
A typical agent setup combines guard hooks (to enforce resource limits) with a stop tool (to allow the model to signal task completion):- The model calls
SubmitAnswerTool, which throwsAgentStopException - The step count reaches 20
- Cumulative token usage exceeds 16,000
- Wall-clock time exceeds 60 seconds
- The model produces a final response with no tool calls (natural completion)
Cooperative Cancellation
TheUseCooperativeCancellation capability lets external code request that a running agent stop — without subclassing AgentLoop or writing custom hook logic.
Cancellation is cooperative and checkpoint-based: the loop checks for a signal at BeforeExecution and BeforeStep. It will not interrupt an in-flight LLM call or tool execution mid-stream. If the agent is between steps when the request arrives, it stops cleanly on the next checkpoint.
Basic Usage
InMemoryCancellationSource also exposes reset() and isCancellationRequested() for inspection and reuse across executions.
Custom Cancellation Sources
ImplementCanProvideCancellationSignal to integrate any external cancel mechanism — a Redis key, database flag, HTTP endpoint, or PHP signal handler:
AgentState, so you can scope cancellation to a specific agent ID, execution ID, or session.
Cancellation vs. Hard Interruption
Unlike thread-based cancellation tokens (e.g.CancellationToken in .NET or context.Context in Go), cooperative cancellation only stops the loop at safe checkpoints. An ongoing HTTP request to the LLM or a running tool will complete before the loop checks for the signal.
If you need to cancel mid-request, that requires interrupting the underlying HTTP transport — which is outside the scope of this capability.
Quick Reference
| I want to… | Use… |
|---|---|
| Stop after N steps | UseGuards(maxSteps: N) or register StepsLimitHook directly |
| Stop after N tokens | UseGuards(maxTokens: N) or register TokenUsageLimitHook directly |
| Stop after N seconds | UseGuards(maxExecutionTime: N) or register ExecutionTimeLimitHook directly |
| Stop on LLM finish reason | UseGuards(finishReasons: [...]) or register FinishReasonHook directly |
| Stop from inside a tool | Throw AgentStopException with a StopSignal |
| Stop from a custom hook | Emit a StopSignal via $state->withStopSignal() |
| Cancel from outside the loop | UseCooperativeCancellation + CanProvideCancellationSignal |
| Override a stop signal | Call $state->withExecutionContinued() in a hook |
| Check why the agent stopped | Inspect $state->executionContinuation()->stopSignals() |
| Check if stop was forced | Call $signal->reason->wasForceStopped() |
| Get human-readable stop info | Call $continuation->explain() |