Skip to main content

Basic Concepts

The Agents package is built around a small set of immutable value objects that represent everything about an agent’s lifecycle. Understanding these concepts is essential before working with any other feature of the system.

AgentLoop

The AgentLoop is the orchestrator at the heart of every agent. It drives a step-based execution cycle: call the LLM, execute any requested tools, evaluate stop conditions, and repeat until the agent is finished. Each iteration of the loop follows a well-defined lifecycle:
BeforeExecution -> [ BeforeStep -> UseTools -> AfterStep -> ShouldStop? ] -> AfterExecution
// @doctest id="7fd1"
The loop begins with a BeforeExecution phase where the execution state is initialized. It then enters a repeating cycle of steps. During each step, the loop fires a BeforeStep hook, hands control to the driver to call the LLM and execute any tool calls, fires an AfterStep hook, and then evaluates whether the agent should stop. If the model responds without requesting any tool calls (a “final response”), or if a stop signal has been emitted by a hook, the loop exits. An AfterExecution phase finalizes the state before it is returned. You can obtain a default loop with sensible defaults using the static constructor:
use Cognesy\Agents\AgentLoop;

$loop = AgentLoop::default();
// @doctest id="4a8c"
This creates a loop wired with the default tool-calling driver, an event dispatcher, and a pass-through interceptor. For more control, use the AgentBuilder to compose a loop with specific capabilities, tools, and hooks.

AgentState

AgentState is an immutable value object that carries everything about an agent’s session and its current execution. Every method that modifies state returns a new instance, leaving the original untouched. This immutability makes the data flow through the loop predictable and safe for inspection at any point. The state is divided into two conceptual layers: Session data persists across executions. It represents the agent’s long-lived identity and accumulated conversation history:
PropertyDescription
agentIdA unique identifier for this agent instance
parentAgentIdThe ID of the parent agent, if this is a sub-agent
contextThe AgentContext holding messages, system prompt, metadata, and response format
llmConfigOptional LLM configuration overrides
executionCountHow many times this agent has been executed
createdAt / updatedAtSession timing timestamps
Execution data is transient. It exists only while the loop is running and is null between executions. The execution property holds an ExecutionState instance that tracks step results, the current step, timing, continuation signals, and the execution status. Creating an initial state is straightforward:
use Cognesy\Agents\Data\AgentState;

$state = AgentState::empty()
    ->withSystemPrompt('You are a helpful assistant.')
    ->withUserMessage('What is the capital of France?');
// @doctest id="19f3"
Because every with* method returns a new instance, you can chain calls fluently. The original $state is never modified. AgentState is the runtime state for a single agent loop. If you need to persist state across HTTP requests or manage multi-turn sessions, see the Session Runtime documentation for AgentSession and SessionRuntime.

AgentContext

AgentContext is the container for all conversation-related data that the agent sends to the LLM. It holds four pieces of information:
  • MessageStore — the sectioned storage for conversation messages. Messages are organized into named sections (the default section holds the main conversation). The driver’s message compiler reads from this store to build the final prompt.
  • System prompt — the instruction text prepended to every LLM call.
  • Metadata — arbitrary key-value pairs that hooks and capabilities can use to pass information through the pipeline without modifying messages.
  • Response format — an optional structured output format that instructs the LLM to respond in a particular schema.
You interact with the context indirectly through AgentState methods:
$state = AgentState::empty()
    ->withSystemPrompt('You are a research assistant.')
    ->withUserMessage('Summarize this article.')
    ->withMetadata('user_id', 42);
// @doctest id="a214"
When the loop calls the LLM, the driver’s message compiler (implementing CanCompileMessages) transforms the current AgentContext into the final Messages collection sent to the inference API. This compilation step is where features like message filtering, trace exclusion, and context summarization are applied.

AgentStep

An AgentStep is an immutable snapshot of a single loop iteration. After the driver calls the LLM and executes any tool calls, the results are bundled into an AgentStep and attached to the state. Each step captures:
PropertyDescription
idA unique AgentStepId for this step
inputMessagesThe messages that were sent to the LLM
outputMessagesThe assistant’s response and any tool result messages
inferenceResponseThe raw LLM response, including token usage and finish reason
toolExecutionsA ToolExecutions collection with the results of each executed tool call
errorsAn ErrorList aggregating any errors from tool execution or the step itself
The step’s type is derived automatically from its contents:
use Cognesy\Agents\Enums\AgentStepType;

$step->stepType(); // AgentStepType::FinalResponse
                   // AgentStepType::ToolExecution
                   // AgentStepType::Error
// @doctest id="c2e6"
A step is classified as ToolExecution when the LLM requested tool calls, Error when any errors occurred during the step, and FinalResponse when the model produced a plain text answer with no tool calls and no errors. You can inspect a step’s tool calls at two levels: requestedToolCalls() returns the tool calls the model asked for, while executedToolCalls() returns only those that were actually run (a tool call can be blocked by a hook before execution).

StepExecution

StepExecution wraps an AgentStep with timing and continuation metadata. When a step completes, the loop bundles the step together with its start and end timestamps and the continuation state at that point, then appends this StepExecution to the completed steps list. This separation keeps the AgentStep focused on what happened during the step (messages, tool calls, errors), while StepExecution records when it happened and whether a stop signal was active:
$stepExecution = $state->lastStepExecution();

$stepExecution->step();          // The underlying AgentStep
$stepExecution->startedAt();     // DateTimeImmutable
$stepExecution->completedAt();   // DateTimeImmutable
$stepExecution->duration();      // float (seconds)
$stepExecution->continuation();  // ExecutionContinuation snapshot
$stepExecution->usage();         // Token usage for this step
// @doctest id="2232"

ExecutionState

ExecutionState tracks the transient state of a single execution run. It is created fresh when the loop starts and is set to null on the AgentState once the execution completes. The execution state manages:
  • Status — one of Pending, InProgress, Completed, Stopped, or Failed (see ExecutionStatus enum).
  • Step history — a StepExecutions collection of all completed steps.
  • Current step — the in-progress AgentStep before it is finalized.
  • Continuation — an ExecutionContinuation object that collects stop signals and tracks whether a continuation has been requested.
  • Timing — execution start and completion timestamps.
You typically access execution data through convenience methods on AgentState rather than working with ExecutionState directly:
$state->status();           // ExecutionStatus::InProgress
$state->stepCount();        // 3
$state->usage();            // Accumulated token usage across all steps
$state->executionDuration();// Total wall-clock time in seconds
$state->shouldStop();       // Whether the loop should terminate
// @doctest id="aae9"
The shouldStop() logic follows a clear priority chain. If a stop signal has been emitted (by a hook such as StepsLimitHook or FinishReasonHook) and no continuation has been explicitly requested, the execution stops. If no stop signal exists but the current step has tool calls, the execution continues to process them. If neither condition applies — meaning the model produced a final response with no tool calls — the execution stops naturally.

ToolExecution

A ToolExecution is an immutable record of a single tool call’s outcome. It captures the tool call that was requested, the result (success or failure), and precise timing:
$toolExec = $state->lastToolExecution();

$toolExec->name();        // 'search_web'
$toolExec->args();        // ['query' => 'PHP 8.4 features']
$toolExec->hasError();    // false
$toolExec->value();       // The successful return value
$toolExec->error();       // null (or a Throwable on failure)
$toolExec->wasBlocked();  // true if a hook blocked execution
$toolExec->startedAt();   // DateTimeImmutable
$toolExec->completedAt(); // DateTimeImmutable
// @doctest id="9987"
Tool executions are collected within each AgentStep via its toolExecutions() method. When a tool call is blocked by a pre-execution hook, the ToolExecution is still recorded, but with a Failure result containing a ToolExecutionBlockedException.