Skip to main content

Agent State Internals

Every agent execution revolves around a single, immutable data structure: AgentState. This object carries the full picture of an agent’s identity, conversation context, and execution progress. Understanding its internal structure is essential for building custom guards, hooks, and persistence layers.

Design Philosophy

AgentState follows two core principles:
  1. Immutability. The class is declared final readonly. Every mutation method (with*, forNextExecution, etc.) returns a new instance, leaving the original untouched. This makes state transitions explicit and safe for concurrent inspection.
  2. Session vs. Execution separation. Some data persists across executions (identity, context, message history), while other data is transient and scoped to a single execution (step results, timing, continuation signals). This split is represented by the nullable ExecutionState property.

AgentState Structure

The following diagram shows the complete object graph:

Session Data (Persists Across Executions)

Session-level properties survive between executions. When you call forNextExecution(), these fields are preserved while execution is reset to null:
  • agentId — A typed UUID (AgentId) that uniquely identifies the agent instance. Generated automatically on construction.
  • parentAgentId — Set when the agent is spawned as a subagent. Enables parent-child correlation in event tracing.
  • createdAt / updatedAt — Timestamps for lifecycle tracking. updatedAt is bumped on every mutation via with().
  • executionCount — Monotonically increasing counter. Incremented by AgentLoop::onBeforeExecution() at the start of each execution. Useful for guards that behave differently on the first execution.
  • llmConfig — Optional LLMConfig override. When set, the driver uses this configuration instead of its default provider settings.
  • context — The AgentContext containing the message history, system prompt, metadata, and response format.

Execution Data (Transient Per Execution)

The execution property holds an ExecutionState that is created fresh at the start of each execution and discarded (set to null) when the execution completes:
  • executionId — A unique ExecutionId for correlation. Generated via ExecutionState::fresh().
  • status — An ExecutionStatus enum tracking the execution lifecycle.
  • stepExecutions — A StepExecutions collection of completed StepExecution objects. Each wraps an AgentStep together with its timing and continuation state.
  • continuation — An ExecutionContinuation that holds stop signals and continuation requests. The agent loop consults this after each step to decide whether to continue or stop.
  • currentStep — The AgentStep currently being processed. Set by the driver via withCurrentStep(), then archived into stepExecutions when withCurrentStepCompleted() is called.

ExecutionStatus Lifecycle

ExecutionStatus is a string-backed enum with five cases: The AgentLoop manages these transitions automatically:

AgentStep Internals

Each step in the execution is represented by an AgentStep — an immutable snapshot of what happened during a single driver invocation:
The step type is derived, not stored. AgentStep::stepType() inspects the step’s contents to determine its type:
  1. If the step has errors (including tool execution errors), the type is AgentStepType::Error.
  2. If the step has requested tool calls, the type is AgentStepType::ToolExecution.
  3. Otherwise, the type is AgentStepType::FinalResponse.
This derivation means you never need to manually set the step type — it is always consistent with the step’s actual contents.

StepExecution Wrapper

When a step is completed, it is wrapped in a StepExecution that bundles the step with timing and continuation data:
This separation keeps AgentStep focused on what happened (messages, tools, errors) while StepExecution owns when it happened and whether the loop should continue.

Message Metadata Tagging

When a step’s output messages are appended to the agent context, AgentState::withCurrentStep() automatically tags each message with metadata:
  • step_id — The AgentStepId of the step that produced the message.
  • execution_id — The ExecutionId of the current execution.
  • agent_id — The AgentId of the agent.
  • is_trace — Set to true for non-final steps (tool execution, error). Final response messages do not carry this flag.
This metadata enables downstream compilers (such as ConversationWithCurrentToolTrace) to filter messages at read-time based on their origin, without modifying the underlying message store.

Key Accessors

AgentState provides a rich set of accessors for inspecting the current state at any point during or after execution:

Identity and Timing

Context

Execution State

Final Output

Continuation and Stop Signals

The agent loop uses ExecutionContinuation to decide whether to keep iterating. After each step, the loop calls $state->shouldStop(), which delegates to:
Stop signals carry a StopReason enum with prioritized cases: Multiple stop signals can coexist. The wasForceStopped() method on StopReason returns true for all reasons except Completed and FinishReasonReceived, which represent natural completion.

ExecutionBudget

ExecutionBudget declares per-execution resource limits. It is defined on an AgentDefinition and applied as a UseGuards capability when the agent loop is built — it is not stored inside AgentState.
All limits are optional — pass null (or omit) for unlimited. You can check whether a budget has any limits set with isEmpty(), or whether all limits have been exhausted with isExhausted(). The ExecutionBudget::unlimited() factory returns a budget with all limits set to null:
Each subagent receives its own declared budget. Recursion depth is controlled separately via SubagentPolicy (maxDepth), not through the budget.

Debugging

AgentState::debug() returns an associative array summarizing the current state — useful for logging or test assertions:

Serialization

All state objects implement toArray() and fromArray() for persistence and hydration. This covers the full object graph — AgentState, ExecutionState, AgentStep, StepExecution, ToolExecution, and ExecutionContinuation:
This is the foundation for session persistence. The SessionStore implementations use toArray() / fromArray() to save and restore agent state between requests or across process boundaries.

Serialization Scope

Key Gotcha: ensureExecution() Creates Fresh State

The private ensureExecution() method returns ExecutionState::fresh() with a new UUID when execution is null. This means calling it twice produces different execution IDs. The AgentLoop handles this correctly, but if you are building custom orchestration, be aware that you must capture and reuse the returned state: