In Agent Runtime, Session, Run, Turn, and Step frequently appear together. Memorizing these definitions by rote easily blurs their boundaries, because different frameworks use slightly different names, and some frameworks don't even expose one of these layers as an explicit object.
A more stable understanding comes from observing the several timescales that coexist in a system.
A single Provider request ending doesn't mean the current Agent's work is done; a Tool Loop stopping doesn't mean the conversation has ended; a browser disconnect certainly doesn't mean backend execution has terminated.
If all these timescales are compressed into messages[] + isLoading, the system can initially function, but cancellation, resumption, approval, concurrent input, and persistence gradually come to rely on implicit agreements.
Multi-layer lifecycles emerge from these constraints.
Starting from a Single Model Call
The simplest model invocation has only one lifecycle:
Input
↓
Model Request
↓
Output
When the request starts, the input is fixed; when it ends, the result is fixed.
If a system only makes such calls, the Provider Request itself is the primary execution boundary. Timeout, cancellation, and errors can all be bound to this request.
Tool Calling changes this.
The model may first return a Tool Call:
Input
↓
Model Request #1
↓
Tool Call
↓
Tool Execution
↓
Tool Result
↓
Model Request #2
↓
Final Output
Model Request #1 has ended, but the current work is clearly not yet complete.
This means a Provider Request can only describe a single model computation, not the Agent's complete progression.
Step: Establishing a Stable Boundary for a Single Model Decision
DeepSeek Harness uses Step to describe a boundary close to a single model decision.[1]
A Step can be abstracted as:
Step
│
├── Build Context
├── Model Request
├── Stream Assistant Output
├── Execute Tool Calls
├── Append Tool Results
└── Decide Next State
One detail worth clarifying: Step is usually not equivalent to a single HTTP Provider Request.
The reason is that after the model produces Tool Calls, these Tool Calls are still direct results of this model decision. If we close the Step immediately when the model stream ends, Tool Execution becomes orphaned intermediate state; if we attribute tool results to the subsequent Step, it weakens observability of "what effects this single model decision produced."
Therefore, putting the Model Decision and its directly produced Tool Work in the same Step creates a more stable execution boundary:
Context N
↓
Model Decision N
↓
Tool Effects N
↓
Step N ends
↓
Context N+1
This boundary is valuable for Runtime.
Before the next Step starts, we can safely process:
- New Steering Messages;
- Context Compaction;
- Hooks;
- Model/Tool configuration changes;
- Cancellation state;
- Runtime-injected information.
Started Model/Tool work doesn't need to be rewritten mid-flight because of these changes.
Thus, Step is closer to Runtime's smallest stable advancement unit.
Why Can't Assistant Message Simply Be Treated as Step
The most intuitive unit in UI is usually one Assistant Message, but it doesn't exactly match Runtime's execution boundary.
One Assistant Message may contain multiple Tool Calls:
Assistant Message
├── text
├── Tool Call A
└── Tool Call B
Runtime also needs to wait for A and B's results before deciding whether to proceed to the next model request.
If we only observe Message, it's hard to determine:
Whether this model decision has completely finished
Whether all Tools have completed
Whether the next Context can be constructed
Whether Steering can be consumed
Therefore, Message is better for expressing conversation content; Step is better for expressing execution lifecycle.
They may be highly correlated, but they address different concerns.
Turn: Why Multiple Steps Still Belong to the Same Piece of Work
With Step, a second question emerges.
The model returns Tool Call the first time, tool execution completes, then requests the model; the model may continue calling tools on the second request; only on the third does it produce final text.
Should these Steps be viewed as separate tasks?
Usually not.
They're still driven by the same continuous work:
Turn
│
├── Step 1
│ ├── Model
│ └── Tool A
│
├── Step 2
│ ├── Model
│ ├── Tool B
│ └── Tool C
│
└── Step 3
└── Final Response
Turn describes exactly this continuous advancement interval.
DeepSeek Harness checks the next-step Inbox after step/end. If there's still input to consume in the current work, it continues to the next Step. Before naturally stopping, it also passes through agent/turn-stopping, allowing Hooks to re-insert work before the Turn truly ends; only when there's no remaining work does it write turn/end.[1][2]
Pi doesn't expose Step as a first-class object with the same name, but runLoop()'s structure reflects the same time hierarchy: the inner loop continuously processes Tool Calls and Steering Messages, and after the current continuous work ends, the outer loop processes Follow-up.[3]
Turn's end condition can't be determined solely by "the model has returned text." A more accurate condition is:
There's no content that must be immediately advanced in the current continuous work
This is also why Steer and Follow-up need different boundaries:
Steer
Enters the next Step after the current Turn
Follow-up
Waits for the current Turn to reach its stopping boundary
The third and fourth articles will continue verifying this relationship from Pi's source code.
Session: What Needs to Be Retained After Turn Ends
After Turn ends, the Agent may have gone idle, but the conversation hasn't disappeared.
The next user input still needs to inherit existing history, branches, Memory, or session configuration.
Therefore, a lifecycle noticeably longer than Turn is needed:
Session
│
├── Turn A
├── Turn B
├── Turn C
└── ...
Session is closer to the long-term conversation boundary.
It usually saves:
Historical Messages / Events
Session metadata
Current branch or lineage
Persistent state
Long-term Memory references
Results from multiple executions
Pi Coding Agent saves Session as JSONL, and uses id / parentId to express tree history; multiple branches can exist in a single Session file.[4]
DeepSeek Harness's Session uses an append-only Event Log, and Session Log serves as the durable source of truth for live Agents.[2]
Their persistence structures differ greatly, but their lifecycle position is consistent: Session sits above a single Agent Loop.
Why Runtime State During Execution Shouldn't All Go Into Session
Session's lifecycle is long, so it's suitable for saving facts that need to persist across processes and recoveries.
Temporary objects in current execution are different, for example:
AbortController
A half-streamed Assistant Message
Current network connection
A Tool's in-memory handle
Current active promise
These objects usually have no long-term persistence significance and can't even be serialized.
If Session simultaneously承担 long-term facts and in-process temporary state, recovery logic becomes very difficult: the system needs to determine which fields can be reconstructed, which have already expired, and which are just references left by some old process.
A more stable boundary is:
Session
Saves durable facts
Runtime State
Saves current in-process active state
When persisting, valuable state in Runtime can be transformed into Events, Checkpoints, or Run Status, but there's no need to persist the entire Runtime object.
Why Run Is Still Needed
By now we have Session, Turn, and Step. Many Agent Cores are sufficient at this layer.
Web products and task-oriented applications still frequently introduce Run,原因是 product-layer execution management.
After one user operation triggers an Agent, the system usually needs a queryable execution entity:
QUEUED
RUNNING
WAITING_APPROVAL
PAUSED
COMPLETED
FAILED
CANCELLED
It may also need to record:
runId
startedAt / endedAt
triggerMessageId
cost / tokens
error
approval state
parentRunId
childRunIds
resume cursor
These states clearly don't belong to a single Step.
They're also not suitable to hang directly on the entire Session, because a Session can go through multiple independent executions:
Session S1
│
├── Run R1 completed
├── Run R2 failed
└── Run R3 running
If Session only has one global status, it can't preserve each execution's independent lifecycle.
Therefore, Run mainly solves: how to separate a manageable, observable, recoverable application-layer execution from the long-term Session.
Why Run Doesn't Necessarily Belong to Agent Core
A common misconception needs to be avoided here: since Run is useful, solidify it directly into the Agent Loop.
This would bring a lot of product semantics into the execution kernel, for example:
Approval
Billing
Task queue
Retry strategy
Parent/child tasks
SLA
Page recovery
Audit
These capabilities have no direct relationship with how models call Tools.
DeepSeek Harness's documentation gives a valuable boundary for this: whenIdle() observes the interval from the Agent's current activity until quiescence, and only when the caller explicitly owns this interval is it suitable to model it as a run.[2]
Pi's Agent also has ActiveRun, which saves promise, resolve, and AbortController, but it only manages the current active processing interval and doesn't bear the complete semantics of business-layer persistent Run.[5]
Therefore, the following layering can be adopted:
Agent Core
├── Step
├── Turn
├── Tool
├── Input Delivery
└── Cancel
Application Runtime
├── Run ID
├── Durable Status
├── Approval
├── Retry / Resume
├── Observability
└── Parent / Child Run
Run can exist, but there's no need to force all Agent Cores to understand it.
Where Turn and Run Are Most Easily Confused
Both can be understood as "one piece of work," but they focus on different things.
Turn is a continuous work interval in Agent execution semantics; Run is more about product and scheduling semantics.
For example, a Run may pause due to approval:
Run R1
│
├── Turn A
│ └── Requests sensitive operation
│
├── WAITING_APPROVAL
│
└── Turn B
└── Continues after approval
In this case, one Run can span multiple Turns.
Some simpler products can also choose "one Run corresponds to one Turn." The object model doesn't require complexity; the key is that conceptual boundaries can accommodate future pause, resume, and scheduling needs.
Therefore, this series uses:
Turn
Continuous advancement boundary in Agent Core
Run
A persistent management boundary in the application layer
These definitions are more useful than requiring all frameworks to use the same names.
Connection Is Another Independent Lifecycle
Web Agents have an even shorter and less stable lifecycle: client connection.
Connection A
───────────>
Connection B
─────────────>
Run
──────────────────────────────────>
Session
────────────────────────────────────────────>
Connection is only responsible for real-time transmission. Page refresh, network switching, or SSE reconnection will cause Connection to change, but Run and Session can remain unchanged.
If we bind them together:
connection close
→ run cancel
→ session activity lost
The system will struggle to support true long-running tasks and recovery.
Therefore, Web Agents should at least clearly distinguish:
Connection
Communication lifecycle
Run
Execution lifecycle
Session
Long-term conversation lifecycle
The subsequent Web Runtime unit will specifically address how to restore Run's real-time view after Connection changes.
Should Agent and Session Be One-to-One?
After determining multi-layer lifecycles, there's still the binding problem between Agent and Session.
DeepSeek Harness currently tends toward one live Agent driving one Session. The public Agent handle directly holds session, inbox, status, and agent-scoped ctx; the relationship is strong.[2]
The advantage of this design is straightforward recovery path:
Session
↓
resume live Agent
The Agent's Prompt, Tools, adapter, and Session history can remain consistent.
Other products may allow the same Session to be participated by multiple AgentDefinitions:
Session S1
│
├── Run R1 → CodingAgent v3
├── Run R2 → ReviewAgent v2
└── Run R3 → CodingAgent v3
This mode is more suitable for multi-role collaboration or orchestration, but each Run must record the actual Agent configuration used, otherwise after history recovery it's hard to determine the Prompt, Tools, and Model Policy at that time.
Both structures are valid. What's important to avoid is letting Session arbitrarily switch to incompatible Agent configurations without version information.
Cancellation, Errors, and Recovery — Where Should Each Land?
Whether a lifecycle model is reasonable can be checked in reverse through exceptional paths. During normal execution, many objects may seem mergeable; once cancellation, errors, and recovery appear, boundaries quickly surface.
Let's look at cancellation first.
If the user only wants to terminate the current Provider Request, the cancellation scope can stop at Model Call; if the current Tool and subsequent Step have no further execution meaning, the cancellation scope should cover the current Turn; if the product manages this segment of tasks as a persistent Run, it also needs to set Run status to CANCELLED.
Abort Provider Request
↓
May only affect current Model Call
Cancel current Agent work
↓
End current Turn / active execution
Cancel product task
↓
Run = CANCELLED
These actions can be triggered by the same user operation, but internally there are still multi-layer state changes.
Errors are similar. Provider timeout may only need to retry the current Step; Tool failure may be handed back to the model as Tool Result for continued processing; some unrecoverable errors directly end the Turn; after reaching product retry limits, Run enters FAILED. Session usually still exists, and the user can later initiate a new Run.
Provider error
↓ retry?
Step
↓ recoverable?
Turn
↓ unrecoverable?
Run = FAILED
Session remains
If there's only one global session.status, these error layers are hard to express. One Tool failure might mark the entire conversation as failed, when the user can clearly continue using the same Session.
Recovery paths also verify boundaries.
Page refresh only needs to restore Connection; process restart needs to rebuild Runtime; when Run is waiting for approval, restore Run state and pending work; Session is responsible for providing long-term history.
Browser reconnect
→ restore connection view
Process restart
→ rebuild runtime from durable facts
Run resume
→ locate execution status / cursor
Session resume
→ load long-lived conversation state
Therefore, the significance of lifecycle objects is not only evident in normal paths, but also in that each layer has different failure, cancellation, and recovery conditions.
Who Creates, Who Ends, Who Owns State
Three questions can also check object boundaries:
Who creates it?
Who has the right to end it?
Who persists its state?
Step is usually created by Agent Loop and ends naturally; Turn is also controlled by Runtime for its continuous advancement. Run is often created by application or scheduling layer, and Runtime only reports status. Session is usually created by product session layer and persisted long-term.
If one object's creator, ender, and state owner are completely different, it likely spans multiple architectural layers and needs explicit interfaces to connect, rather than being simply merged into some core class.
Why Context Isn't in This Persistent Lifecycle Tree
Session, Run, Turn, and Step can all discuss "when to start, when to end." Context's nature is different.
Context is closer to a computational view constructed before some Step initiates a model request.
After Pi's Agent.prompt() enters runPromptMessages(), it first calls:
createContextSnapshot()
The current implementation returns:
return {
systemPrompt: this._state.systemPrompt,
messages: this._state.messages.slice(),
tools: this._state.tools.slice(),
}
Then it passes AgentContext to runAgentLoop().[5]
This Prompt is appended to Loop Local Context, and before actually requesting Provider, it can still go through transformContext, convertToLlm, and other processing.
Therefore, a more accurate relationship is:
Session / Agent State
│
▼
Context Snapshot
│
▼
Current Turn / Step
│
▼
Provider Context
Context continuously changes with execution; it doesn't need to be independently saved as a long-term entity like Session.
What persistence systems truly need to save is facts that can reconstruct Context.
This will be the core issue of the second unit.
Why Can't We Keep Only Session and Step
From the perspective of abstract minimization, one can question whether intermediate layers are all necessary.
For example:
Session
└── Step*
This structure is feasible in implementation, but the problem is that many control semantics will lose stable attribution.
When does Follow-up execute after which group of Steps completes?
What segment of work does approval pause?
After a user-triggered execution fails, where should the error be attached?
How should duration, cost, and status of a long-running task be queried?
When systems start needing these capabilities, Turn and Run naturally emerge.
Therefore, multi-layer lifecycles aren't fixed dogma. A more reasonable principle is:
When two types of state have different creation, ending, recovery, or control conditions, they should be considered as different lifecycle objects.
If a product lacks a certain type of requirement, it can omit the corresponding object.
Lifecycle Concepts Don't Have to Directly Map to Database Tables
After understanding Session, Run, Turn, Step, another common question is whether persistence entities need to be created for each layer.
The answer depends on what granularity the product needs to query and recover.
Session almost always needs persistence because it carries long-term conversation. Run, if it needs cross-connection execution, approval, retry, or independent status queries, also usually deserves a persistent ID.
Turn and Step don't necessarily need independent tables.
One simple system can only save Messages:
sessions
messages
Turn / Step are just execution concepts in Runtime memory.
If the system needs complete audit, replay, cost statistics, and fault location, it can write lifecycle as Events:
session_events
├── turn/start
├── step/start
├── assistant/message
├── tool/call
├── tool/result
├── step/end
└── turn/end
This way Turn and Step still have clear semantics, but don't each need to maintain mutable row state. DeepSeek Harness's Session Event Log is close to this direction.[2]
Another type of task platform may indeed need:
runs
run_steps
Because each Step needs to independently display status, retry, or billing. At this point, materializing Step as an entity has product value.
Therefore, object model and storage model should be considered separately.
Conceptual lifecycle
Explains how the system operates
Persistence model
Explains which facts need long-term saving and querying
They can correspond one-to-one, or be expressed through Event Log, Projection, or aggregated fields. Mechanically creating tables for each concept just to make the architecture diagram "complete" usually only increases synchronization costs.
The Longer the Lifecycle, the Higher the Persistence Requirements
A general rule can be derived:
Model Call / Step
More runtime-oriented, can be recorded by events
Turn
Chosen based on audit and recovery needs
Run
Task-oriented products usually need persistent state
Session
Long-term conversations usually must be persisted
This rule also explains why Agent Core cares more about Step / Turn, while Web products care more about Run / Session. They're observing the same execution system at different timescales.
Unifying the Entire Model with Time Scales
Finally, these objects can be placed on the same time diagram:
Session
────────────────────────────────────────────────────────>
Run A Run B
─────────────────> ────────────────>
Turn 1 Turn 2 Turn 3
───────> ───────> ───────>
Step Step Step Step Step
───> ───> ───> ───> ───>
Model Call
─────>
Connection A
──────────>
Connection B
───────────>
They respectively answer:
- Model Call: when a single Provider computation ends;
- Step: when a single model decision and its direct Tool Effects end;
- Turn: when the current continuous Agent work reaches its stopping boundary;
- Run: when an application-layer execution has a final state;
- Session: when this long-term conversation ends or gets archived;
- Connection: when a client real-time connection disconnects.
This division will directly affect subsequent design.
Which boundary Steer should enter, which boundary Follow-up waits for, which layer Abort terminates, which facts Session should save when recovering, and whether Web reconnection should restore Run or create a new Run all depend on the lifecycle model here.
The final design decision left by Part 2 is:
In Agent systems, object boundaries should be primarily determined by lifecycle differences. Session, Run, Turn, and Step respectively have different start, end, control, and recovery conditions; names are just expressions of these boundaries.
The next article enters Pi's actual call chain, observing how Agent.prompt() establishes activity intervals, how it constructs Context, how it advances two-layer Loops, and how Events synchronize lower-layer execution results back to the outer Agent State.
References
[1] DeepSeek Harness Agent Lifecycle: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/agent-lifecycle.md
[2] DeepSeek Harness Core Subsystem: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/core.md
[3] Pi agent-loop.ts: https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent-loop.ts
[4] Pi Coding Agent Sessions: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md
[5] Pi Agent source: https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/agent.ts