Skip to main content

Controlling the Loop

The AgentLoop exposes two methods for running an agent: execute() for simple run-to-completion workflows, and iterate() for step-by-step observation. Both operate on an immutable AgentState and return the resulting state after the agent finishes.

execute() vs iterate()

Running to Completion

The execute() method runs the full loop and returns the final state in a single call. This is the right choice for most application code where you simply need the agent’s answer:
use Cognesy\Agents\AgentLoop;
use Cognesy\Agents\Data\AgentState;

$loop = AgentLoop::default();

$state = AgentState::empty()
    ->withSystemPrompt('You are a helpful assistant.')
    ->withUserMessage('What are the three laws of thermodynamics?');

$finalState = $loop->execute($state);

echo $finalState->finalResponse()->toString();
// @doctest id="b571"
Internally, execute() is a thin wrapper around iterate() — it simply consumes the iterator and returns the last yielded state.

Stepping Through Execution

The iterate() method returns a generator that yields the state after each completed step. This gives you the opportunity to observe progress, log intermediate results, update a UI, or apply custom logic between steps:
foreach ($loop->iterate($state) as $stepState) {
    $step = $stepState->currentStepOrLast();
    $type = $step?->stepType();

    echo sprintf(
        "Step %d: %s (%d tokens)\n",
        $stepState->stepCount(),
        $type?->value ?? 'unknown',
        $step?->usage()->total() ?? 0,
    );
}
// @doctest id="973b"
Each yielded $stepState is a complete AgentState snapshot. You can inspect messages, tool executions, errors, and token usage at every point in the agent’s run. The final yield includes the post-execution state after the AfterExecution hooks have fired. Use execute() for straightforward application logic. Use iterate() when you need progress updates, streaming indicators, step-level logging, or any form of real-time observation.

Inspecting State After Execution

Once the loop finishes, the returned AgentState provides a comprehensive set of accessors to understand what happened during the run.

Execution Summary

$state->status();             // ExecutionStatus::Completed
$state->stepCount();          // Number of completed steps
$state->executionDuration();  // Total wall-clock time (seconds)
$state->usage();              // Accumulated token usage across all steps
$state->executionCount();     // How many times this agent has been executed
// @doctest id="0974"

Step History

Every completed step is recorded as a StepExecution in the execution’s step history. You can iterate over all steps to review the full trace of the agent’s reasoning:
foreach ($state->stepExecutions()->all() as $stepExecution) {
    $step = $stepExecution->step();

    echo sprintf(
        "Step [%s]: %s (%.2fs)\n",
        $step->stepType()->value,
        $step->outputMessages()->toString(),
        $stepExecution->duration(),
    );
}
// @doctest id="db04"
For quick access to the most recent step:
$state->lastStep();              // The last completed AgentStep
$state->lastStepType();          // AgentStepType enum value
$state->lastStepUsage();         // Token usage for the last step
$state->lastStepDuration();      // Duration of the last step (seconds)
$state->lastStepErrors();        // ErrorList from the last step
// @doctest id="d4cc"

Tool Execution Details

When the agent used tools during its run, you can drill into the execution details of each tool call:
$toolExec = $state->lastToolExecution();

if ($toolExec !== null) {
    echo $toolExec->name();       // Tool name, e.g. 'search_web'
    echo $toolExec->hasError();   // Whether the tool call failed
    echo $toolExec->value();      // The return value on success
}
// @doctest id="9682"
To see all tool executions from the last step:
foreach ($state->lastStepToolExecutions()->all() as $toolExec) {
    echo sprintf(
        "%s(%s) -> %s\n",
        $toolExec->name(),
        json_encode($toolExec->args()),
        $toolExec->hasError() ? 'ERROR: ' . $toolExec->errorMessage() : 'OK',
    );
}
// @doctest id="f7a0"

Stop Reason

Every execution ends for a reason. The stop reason tells you whether the agent completed naturally, hit a limit, encountered an error, or was stopped by an external request:
use Cognesy\Agents\Continuation\StopReason;

$reason = $state->stopReason(); // StopReason enum

match ($reason) {
    StopReason::Completed           => 'Agent finished naturally',
    StopReason::FinishReasonReceived=> 'LLM signaled completion',
    StopReason::StepsLimitReached   => 'Hit the maximum step count',
    StopReason::TokenLimitReached   => 'Exceeded token budget',
    StopReason::TimeLimitReached    => 'Exceeded time limit',
    StopReason::RetryLimitReached   => 'Hit the maximum retry count',
    StopReason::StopRequested       => 'A hook requested a stop',
    StopReason::ErrorForbade        => 'An error prevented continuation',
    StopReason::UserRequested       => 'The user requested a stop',
    default                         => 'Unknown reason',
};
// @doctest id="3c1d"
You can also retrieve the full stop signal for additional context:
$signal = $state->stopSignal();

$signal->reason;   // StopReason enum
$signal->message;  // Human-readable explanation
$signal->context;  // Array of contextual data
$signal->source;   // The class that emitted the signal
// @doctest id="b92a"

Reading the Response

AgentState provides two convenience methods for extracting the agent’s output, each suited to different situations.

finalResponse()

Returns the output messages from the last step, but only if that step was a FinalResponse (the model answered without requesting tool calls). If the execution ended mid-tool-use or with an error, this returns an empty Messages collection:
if ($state->hasFinalResponse()) {
    echo $state->finalResponse()->toString();
}
// @doctest id="fda1"

currentResponse()

Returns the most recent visible output regardless of step type. It first checks for a final response; if none exists, it falls back to the output of the current or last step. This is useful during iterate() loops where you want to show the latest output even if the agent has not finished:
echo $state->currentResponse()->toString();
// @doctest id="7d52"
A typical pattern after execution combines both:
$text = $state->hasFinalResponse()
    ? $state->finalResponse()->toString()
    : $state->currentResponse()->toString();
// @doctest id="3771"

Listening to Events

The AgentLoop dispatches events at every significant point in the execution lifecycle. You can subscribe to specific event types or listen to all events with a wiretap.

Subscribing to Specific Events

Use onEvent() to register a listener for a particular event class. The listener receives the fully-typed event object:
use Cognesy\Agents\Events\AgentStepCompleted;

$loop->onEvent(AgentStepCompleted::class, function (AgentStepCompleted $event) {
    echo sprintf(
        "Step %d: %d tokens, finish=%s (%.2fms)\n",
        $event->stepNumber,
        $event->usage->total(),
        $event->finishReason?->value ?? 'n/a',
        $event->durationMs,
    );
});
// @doctest id="8f1b"

Wiretap (All Events)

Use wiretap() to observe every event the loop dispatches. This is invaluable for debugging and logging:
$loop->wiretap(function (object $event) {
    echo get_class($event) . "\n";
});
// @doctest id="6902"

Available Events

The loop emits the following events during execution:
EventWhen
AgentExecutionStartedThe loop begins a new execution
AgentStepStartedA new step is about to begin
InferenceRequestStartedAn LLM request is being sent
InferenceResponseReceivedAn LLM response has arrived
ToolCallStartedA tool call is about to execute
ToolCallCompletedA tool call has finished
ToolCallBlockedA hook blocked a tool call
AgentStepCompletedA step has finished (includes usage and timing)
ContinuationEvaluatedThe loop evaluated whether to continue
StopSignalReceivedA stop signal was emitted
TokenUsageReportedToken usage was recorded for a step
AgentExecutionStoppedThe loop is stopping (includes stop reason)
AgentExecutionCompletedThe execution has fully finished
AgentExecutionFailedThe execution ended with an error
Events are dispatched through the loop’s EventDispatcher. If you are using the AgentBuilder, the builder can wire a parent event handler so that events propagate up to your application’s event system.

Debugging Execution

For quick diagnostics, AgentState provides a debug() method that returns an array summarizing the execution:
$info = $state->debug();

// [
//     'status'         => ExecutionStatus::Completed,
//     'executionCount' => 1,
//     'hasExecution'   => true,
//     'executionId'    => '550e8400-e29b-41d4-a716-446655440000',
//     'steps'          => 3,
//     'continuation'   => 'No Stop Signals; Continuation Requested: No',
//     'hasErrors'      => false,
//     'errors'         => ErrorList(...),
//     'usage'          => ['input' => 150, 'output' => 42],
// ]
// @doctest id="7ffe"
This is particularly useful when logging or when you need a quick overview of what happened without drilling into individual steps.