Session, Run, Turn, and Step often appear together in Agent Runtime. Simply memorizing these definitions makes it easy to lose sight of their boundaries because naming conventions differ across frameworks, and some frameworks do not even model one of these layers as an explicit object.
A more reliable way to understand them is to look at the several time scales that coexist within a system.
The end of a Provider Request does not mean the Agent's current work has ended; the end of a Tool Loop does not mean the session has ended; and a browser disconnection certainly does not imply that backend execution has terminated.
A system can still work initially if all these time scales are collapsed into messages[] + isLoading, but cancellation, recovery, approvals, concurrent input, and persistence will gradually come to rely on implicit conventions.
Multi-level lifecycles emerge gradually from these constraints.
Starting with a Model Call
The simplest model call has only one lifecycle:
Input
↓
Model Request
↓
Output
The input is fixed when the request begins, and the result is fixed when it ends.
If the system only makes calls like this, the Provider Request itself is the primary execution boundary. Timeouts, cancellation, and errors can all be attached to that 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 already ended, but the current work is clearly not finished.
This means a Provider Request can describe only a single model computation, not the Agent's complete progression.
Step: Establishing a Stable Boundary for Each Model Decision
DeepSeek Harness uses Step to describe a boundary close to a single model decision. [1]
A Step can be abstracted as follows:
One detail is worth making explicit: a Step usually is not equivalent to a single HTTP Provider Request.
The reason is that after the model produces Tool Calls, those Tool Calls remain direct results of the current Model Decision. If the Step closes immediately when the model stream ends, Tool Execution becomes an unowned intermediate state. Assigning the tool results to a later Step, meanwhile, weakens the observability of which effects a single Model Decision produced.
Putting the Model Decision and the Tool Work it directly produces into the same Step therefore creates a more stable execution boundary:
Context N
↓
Model Decision N
↓
Tool Effects N
↓
Step N ends
↓
Context N+1
This boundary is valuable to the Runtime.
Before the next Step begins, the system can safely handle:
- A new Steering Message;
- Context Compaction;
- Hooks;
- Changes to Model or Tool configuration;
- Cancellation state;
- Information injected at runtime.
Model or Tool work already underway does not need to be rewritten midway because of these changes.
A Step is therefore closer to the Runtime's smallest stable unit of forward progress.
Why an Assistant Message Cannot Simply Be Treated as a Step
The most intuitive unit in the UI is usually an Assistant Message, but it does not exactly match the Runtime's execution boundary.
A single Assistant Message may contain multiple Tool Calls:
Assistant Message
├── text
├── Tool Call A
└── Tool Call B
The Runtime still has to wait for the results from A and B before deciding whether to issue the next Model Request.
If all you observe is the Message, it is difficult to determine:
这次模型决策是否已经完整结束
所有 Tool 是否都已经完成
下一次 Context 是否已经可以构造
Steering 是否可以被消费
A Message is therefore better suited to representing conversational content, while a Step is better suited to representing the execution lifecycle.
The two may be closely related, but they address different problems.
Turn: Why Multiple Steps Still Belong to the Same Unit of Work
Once we have Steps, a second question remains.
The model returns a Tool Call on the first request, the tool runs, and the model is called again. The second time, the model may call more tools; only the third response may contain the final text.
Should each of these Steps be viewed as a separate task?
Generally, no.
They are still driven by the same continuous stretch of work:
A Turn describes this continuous interval of progress.
After step/end, DeepSeek Harness checks the next-step Inbox. If input still needs to be consumed as part of the current work, it proceeds to the next Step. Before stopping naturally, the flow also passes through agent/turn-stopping, allowing a Hook to insert more work before the Turn truly ends. Only when no work remains does it write turn/end. [1][2]
Pi does not expose a first-class Step object under the same name, but the structure of runLoop() reflects the same temporal hierarchy: the inner loop continuously handles Tool Calls and Steering Messages, and once the current continuous stretch of work ends, the outer loop handles Follow-up. [3]
The end condition for a Turn cannot rely solely on whether the model has returned text. A more accurate condition is:
当前连续工作已经没有必须立即推进的内容
This is also why Steer and Follow-up require different boundaries:
Steer
进入当前 Turn 后续最近的 Step
Follow-up
等待当前 Turn 到达停止边界
The third and fourth articles will continue verifying these relationships using Pi's source code.
Session: What Must Be Preserved After a Turn Ends
After a Turn ends, the Agent may already be idle, but the session has not disappeared.
The next user input still needs to inherit the existing history, branches, Memory, and session configuration.
This calls for a lifecycle noticeably longer than a Turn:
Session
│
├── Turn A
├── Turn B
├── Turn C
└── ...
A Session maps more closely to a long-lived conversation boundary.
It is usually responsible for storing:
历史 Message / Event
会话元数据
当前分支或 lineage
持久化状态
长期 Memory 引用
多次执行产生的结果
Pi Coding Agent saves Sessions as JSONL and uses id / parentId to represent tree-shaped history. A single Session file may contain multiple branches. [4]
DeepSeek Harness Sessions instead use an append-only Event Log, with the Session Log serving as the durable source of truth for a live Agent. [2]
Although their persistence structures differ substantially, their lifecycle positions are the same: a Session sits above a single Agent Loop execution.
Why Execution-Time State Should Not All Live in the Session
A Session has a long lifecycle, so it is suitable for facts that must survive across processes and recovery.
Temporary objects in the current execution are different, for example:
AbortController
正在 Streaming 的半条 Assistant Message
当前网络连接
某个 Tool 的内存句柄
当前 active promise
These objects generally have no long-term persistence value and may not even be serializable.
If a Session is responsible for both long-lived facts and in-process transient state, recovery logic becomes extremely difficult. The system must determine which fields can be reconstructed, which have expired, and which are merely references left behind by an older process.
A more stable boundary is:
Session
保存 durable facts
Runtime State
保存当前进程内活动状态
When persisting state, valuable Runtime state can be converted into Events, Checkpoints, or Run Status, without persisting the entire Runtime object.
Why a Run Is Still Needed
At this point, we have Session, Turn, and Step. For many Agent Cores, this is sufficient.
Web products and task-oriented applications, however, often still introduce a Run because of execution management at the product layer.
Once a user action 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 do not belong to an individual Step.
Nor are they suitable for attaching directly to an entire Session, because one Session may go through multiple independent executions:
Session S1
│
├── Run R1 completed
├── Run R2 failed
└── Run R3 running
If a Session has only one global status, it cannot retain the independent lifecycle of each execution.
A Run therefore primarily addresses how to separate a manageable, observable, and recoverable application-layer execution from a long-lived Session.
Why a Run Does Not Necessarily Belong to Agent Core
A common misconception needs to be avoided: because a Run is useful, it should be built directly into the Agent Loop.
This would pull a substantial amount of product semantics into the execution core, including:
审批
计费
任务队列
重试策略
父子任务
SLA
页面恢复
审计
These capabilities have no direct relationship to how a model calls Tools.
The DeepSeek Harness documentation provides a valuable boundary here: whenIdle() observes the interval from the Agent's current activity through quiescence. Only when the caller explicitly owns that interval is it appropriate to model it as a run. [2]
Pi's Agent internals also include an ActiveRun that stores a promise, resolve, and an AbortController. However, it manages only the current active processing interval and does not carry the full semantics of a business-layer durable Run. [5]
The following layering is therefore one viable approach:
A Run can exist without forcing every Agent Core to understand it.
Where Turn and Run Are Most Easily Confused
Both can be understood as a single unit of work, but they focus on different things.
A Turn is a continuous work interval within the Agent's execution semantics; a Run leans more toward product and scheduling semantics.
For example, a Run may pause for approval:
Run R1
│
├── Turn A
│ └── 请求执行敏感操作
│
├── WAITING_APPROVAL
│
└── Turn B
└── 审批通过后继续
In this case, one Run can span multiple Turns.
Other, simpler products may also choose to map one Run to one Turn. The object model does not have to become needlessly complex; what matters is that the conceptual boundaries can accommodate future requirements for pausing, resuming, and scheduling.
This series therefore adopts:
Turn
Agent Core 中连续推进的工作边界
Run
应用层可持久管理的一次执行边界
These definitions are more useful than requiring every framework to use the same names.
Connection Is a Separate Lifecycle
Web Agents also have a shorter and less stable lifecycle: the client connection.
A Connection is responsible only for real-time transport. A page refresh, network switch, or SSE reconnection replaces the Connection, while the Run and Session can remain unchanged.
If they are bound together:
connection close
→ run cancel
→ session activity lost
the system will struggle to support genuinely long-running tasks and recovery.
A Web Agent should therefore, at minimum, clearly distinguish between:
Connection
通信生命周期
Run
执行生命周期
Session
长期会话生命周期
A later Web Runtime unit will focus specifically on restoring a Run's live view after its Connection changes.
Should an Agent and Session Have a One-to-One Relationship?
Once the multi-level lifecycle is established, another issue emerges: how an Agent is bound to a Session.
DeepSeek Harness currently leans toward one live Agent driving one Session. Its public Agent handle directly holds the session, inbox, status, and agent-scoped ctx, creating a strong relationship between them. [2]
The advantage of this design is a direct recovery path:
Session
↓
resume live Agent
The Agent's Prompt, Tools, adapters, and Session history can remain consistent.
Another class of products may allow multiple AgentDefinition instances to participate in the same Session:
Session S1
│
├── Run R1 → CodingAgent v3
├── Run R2 → ReviewAgent v2
└── Run R3 → CodingAgent v3
This pattern is better suited to multi-role collaboration or orchestration. However, every Run must record the Agent configuration actually used; otherwise, after restoring the history, it is difficult to determine the Prompt, Tools, and Model Policy that were in effect at the time.
Both structures are valid. What must be avoided is allowing a Session to switch arbitrarily between incompatible Agent configurations without version information.
Which Layer Should Handle Cancellation, Errors, and Recovery?
You can validate a lifecycle model in reverse by examining exceptional paths. During normal execution, many objects may look as though they could be merged. Once cancellation, errors, and recovery come into play, their boundaries quickly become apparent.
Start with cancellation.
If the user only wants to terminate the current Provider Request, the cancellation scope can stop at the Model Call. If the current Tool and any subsequent Steps no longer need to run, the cancellation scope should cover the current Turn. If the product manages this stretch of work as a durable Run, the Run status must also be written as CANCELLED.
A single user action may trigger all these actions, but multiple layers of state still change internally.
Errors are similar. A Provider timeout may require only a retry of the current Step. A Tool failure may be handed back to the model as a Tool Result so that the model can continue handling it. Certain unrecoverable errors end the Turn directly. Only after the product's retry limit is reached does the Run enter FAILED. The Session usually still exists, and the user can start a new Run later.
Provider error
↓ retry?
Step
↓ recoverable?
Turn
↓ unrecoverable?
Run = FAILED
Session remains
With only one global session.status, these layers of error are difficult to express. A Tool failure might mark the entire session as failed, even though the user could clearly continue using the same Session later.
Recovery paths validate the boundaries in the same way.
A page refresh only requires restoring the Connection. A process restart requires rebuilding the Runtime. When a Run is awaiting approval, its status and pending work must be restored. The Session provides the long-term history.
The significance of lifecycle objects therefore appears not only in the normal path, but also in the fact that each layer has distinct failure, cancellation, and recovery conditions.
Who Creates, Ends, and Owns the State?
Three questions can also be used to inspect an object's boundaries:
谁创建它?
谁有权结束它?
它的状态由谁持久化?
A Step is usually created by the Agent Loop and ends naturally. A Turn is likewise controlled by the Runtime in terms of its continuous progression. A Run is often created by the application or scheduling layer, while the Runtime is responsible only for executing it and reporting status. A Session is usually created by the product's session layer and persisted for the long term.
If an object's creator, ender, and state owner are all different, it likely spans multiple architectural layers. Such an object needs to be connected through an explicit interface rather than simply merged into a core class.
Why Context Is Not Part of This Durable Lifecycle Tree
For Session, Run, Turn, and Step, it is natural to ask when they begin and end. Context is different.
A Context is closer to a computational view constructed before a given Step issues a Model Request.
After Pi's Agent.prompt() enters runPromptMessages(), it first calls:
The current implementation returns:
It then passes the AgentContext to runAgentLoop(). [5]
The current Prompt is appended to the Loop Local Context. Before the actual Provider Request, it may also pass through transformContext, convertToLlm, and other processing.
The more accurate relationship is:
Session / Agent State
│
▼
Context Snapshot
│
▼
Current Turn / Step
│
▼
Provider Context
Context changes continuously as execution proceeds. Unlike a Session, it does not need to be stored independently as a long-lived entity.
What the persistence system truly needs to store are the facts from which the Context can be reconstructed.
This becomes the central question of the second unit.
Why We Cannot Keep Only Session and Step
From the perspective of minimizing abstraction, we can question whether every intermediate layer is necessary.
For example:
Session
└── Step*
This structure is feasible to implement, but many control semantics lose a stable home.
Which group of Steps must finish before a Follow-up runs?
Which stretch of work does an approval pause?
After an execution triggered by the user fails, where should the error be attached?
How should the duration, cost, and status of a long-running task be queried?
As a system begins to need these capabilities, Turn and Run emerge naturally.
Multi-level lifecycles are therefore not a fixed dogma. A more reasonable principle is:
When two kinds of state differ in how they are created, ended, recovered, or controlled, consider separating them into different lifecycle objects.
If a product does not need a particular kind of state, it can omit the corresponding object.
Lifecycle Concepts Do Not Always Need to Map Directly to Database Tables
After understanding Session, Run, Turn, and Step, another common question is whether every layer needs a persistent entity.
The answer depends on the granularity at which the product needs to query and recover.
A Session almost always needs to be persisted because it carries a long-lived conversation. A Run usually deserves a persistent ID if it needs to execute across connections, undergo approval or retry, or support independent status queries.
Turns and Steps, however, do not necessarily need their own tables.
A simple system can persist only Messages:
sessions
messages
Turn and Step are merely execution concepts in the Runtime's memory.
If the system needs full auditing, Replay, cost tracking, and troubleshooting, the lifecycle can instead be written as Events:
session_events
├── turn/start
├── step/start
├── assistant/message
├── tool/call
├── tool/result
├── step/end
└── turn/end
Turns and Steps still have clear semantics, but neither needs to maintain its own mutable row state. DeepSeek Harness's Session Event Log is close to this model. [2]
Another class of task platforms may genuinely need:
runs
run_steps
because every Step must independently display its status, support retries, or be billed. Only then does materializing a Step have product value.
The object model and the storage model should therefore be considered separately.
Conceptual lifecycle
说明系统如何运行
Persistence model
说明哪些事实需要长期保存和查询
The two may correspond one-to-one, or they may be expressed through an Event Log, Projection, or aggregate fields. Mechanically creating a table for every concept merely to make the architecture diagram look “complete” usually adds nothing but synchronization overhead.
The Longer the Lifecycle, the Greater the Persistence Requirement
We can derive a rough rule:
This rule also explains why Agent Cores tend to focus on Step and Turn, while Web products focus more on Run and Session. They are observing different time scales of the same execution system.
Unifying the Entire Model Through Time Scales
Ultimately, all of these objects can be placed on the same timeline:
They answer different questions:
- Model Call: When does one Provider computation end?
- Step: When does one model decision and its direct Tool Effects end?
- Turn: When does the current continuous Agent work reach its stopping boundary?
- Run: When does one application-layer execution reach a final state?
- Session: When does this long-lived conversation end or get archived?
- Connection: When does a particular client's real-time connection drop?
This division directly shapes the subsequent design.
Which boundary a Steer enters, which boundary a Follow-up waits for, which layer an Abort terminates, which facts must be preserved when restoring a Session, and whether a Web reconnection restores a Run or creates a new one all depend on this lifecycle model.
The final design judgment from the second article is:
Object boundaries in Agent systems should be determined primarily by lifecycle differences. Session, Run, Turn, and Step have different conditions for starting, ending, controlling, and recovering; the names merely express those boundaries.
The next article enters Pi's actual call chain to examine how Agent.prompt() establishes an active interval, constructs the Context, advances two layers of Loop, and how Events synchronize low-level 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