The layer model
This is the protocol's foundational concept. OGR models agent traffic the way the layered network model models packets — and it is built the way a firewall is: an integration sees one event at a time, the way a firewall sees one IP packet, and the runtime reassembles everything above it and reads everything below it out of the payload.
The model has two axes — the same two a firewall has: entities (the parties, which persist) and a traffic stack (the activity, every unit an episode with a beginning and an end).
The traffic stack
| # | OGR layer | One unit is |
|---|---|---|
| L6 | Session | one conversation |
| L5 | Turn | one instruction → quiescence |
| L4 | Step | one model call: request + response, paired by step_id |
| L3 | Event | one GuardEvent, half a step — the only layer on the wire |
| L2 | Call | one tool call the model asked for |
| L1 | Exec | one real execution on a machine — named by the model, not carried by the contract |
Read downward it is containment — a session holds turns, a turn holds steps, a step holds exactly two events, a response event holds zero or more calls, a call resolves to at most one exec. Read upward it is an observability ladder: the higher the layer, the more reconstruction stands between the wire and the answer.
And the same six layers in the vocabularies you already have — the network stack, tracing spans, and the three agent SDKs:
| # | OGR | Network | OTel GenAI | OpenAI Agents SDK | Claude Agent SDK | LangGraph |
|---|---|---|---|---|---|---|
| L6 | Session | session table | conversation.id | Session id | session_id | thread |
| L5 | Turn | flow | invoke_agent | one Runner.run() | one query() | one invoke() |
| L4 | Step | transport | chat span | generation_span ⚠️ | one round trip ⚠️ | model node |
| L3 | Event | the packet | span start / end | span start / end | AssistantMessage | around invoke() |
| L2 | Call | link | execute_tool | function_span | tool_use | ToolNode |
| L1 | Exec | physical | — | — | the host command | the tool fn |
| — | Agent | host | agent.id | Agent | agent · subagent | the graph |
⚠️ Both agent SDKs call an L4 step a "turn" — one loop iteration, which is
what max_turns counts. An OGR turn is the instruction episode above it.
The detailed mapping, with what to
send as session_hint and why a hook can refuse where a span cannot, is
further down.
The event is the packet
Like a packet, a GuardEvent is a
header — kind (step/request | step/response), step_id, and the
identity four-tuple agent_id · agent_type · agent_workspace · agent_user
(OGR's answer to the firewall's 5-tuple; the fifth coordinate, the tenant,
comes from the API key and never from the payload) — plus a payload: the
raw provider body.
That is all the wire carries. No session ids, no turn or step numbers, no lifecycle marks: coordinates a sender could declare are coordinates a sender could get wrong, so every layer above the event is derived server-side — a firewall does not ask packets which connection they belong to.
Above the packet: reassembly
- A step's two events are paired by the producer-minted
step_id— fragment reassembly. The halves arrive independently and sometimes out of order; each is judged at its own moment: the request before the model sees it, the response before the agent acts on it. - A turn opens at a user instruction and is closed by the runtime: the
next instruction, the body's own
finish_reason, or an idle timeout — a flow table's FIN / RST / timeout. - A session is chained from what requests already carry: each request
holds the whole conversation, so its prefix fingerprints link it to its
predecessor — plus the producer's own optional
session_hintwhen it has one.
Below the packet: parsed, then inferred
- A call is parsed from the response payload; its result arrives inside
the next step's request and is paired back by the provider's call id. The
call belongs to the step that issued it — and it is the unit enforcement
names: a verdict's
findings[].pathsays which call offended, and an integration may refuse only that one. - An exec is what actually ran. No integration observes this layer:
a gateway sees what was asked (the call) and what came back (the result),
never what happened in between — and the wire deliberately carries no exec
kinds. The layer is in the model because the gap between what a call claims
and what an exec does — a tool named
get_weatherthat actually deletes files — is precisely what agent security is about, and a model without the layer cannot even name that blind spot.
A text-only step has no calls and no execs — empty lower layers are normal, like a bare ACK carrying no application data.
The entity axis
| Entity | Network analogue | On the wire |
|---|---|---|
| Tenant | the administrative boundary | the API key (never the payload) |
| Workspace | security zone — one zone, one policy set | agent_workspace |
| Agent | host / endpoint | agent_id (+ agent_type, agent_user) |
An agent is an endpoint, not a layer. Every stack unit is an episode; an agent persists with zero traffic — sessions belong to it the way TCP connections belong to a host. It is addressed by the identity four-tuple every event header carries, and discovered from traffic the way hosts are inventoried from packets.
Your harness already has words for this
Most agents are instrumented, or at least built on an SDK, before they are guarded — so the traffic already has names. Two families of them: the tracing vocabulary (OpenTelemetry's GenAI semantic conventions, and dialects such as OpenInference) and each SDK's own — the OpenAI Agents SDK, the Claude Agent SDK, LangGraph. They describe the same traffic these six layers describe. Here is the correspondence, exactly.
One word, two meanings: a tracing span is a timed operation in a trace; a
verdict's modifications.spans are character offset ranges in a text. Below,
"span" means the first.
The map
| # | OGR | Network (OSI / TCP-IP) | OTel GenAI | OpenAI Agents SDK | Claude Agent SDK | LangGraph |
|---|---|---|---|---|---|---|
| L6 | Session — one conversation | no OSI layer — the firewall's session table, idle aging | gen_ai.conversation.id (no span) | Session / SQLiteSession id; a trace's group_id | the session — session_id, resume, fork | the thread — thread_id + checkpointer |
| L5 | Turn — one instruction → quiescence | no OSI layer — a flow's FIN / RST / timeout | invoke_agent span | one Runner.run() — one trace | one query() prompt, up to its ResultMessage | one invoke() / stream() on the graph |
| L4 | Step — one model call | transport (OSI L4) | the inference span, chat {model} | generation_span / response_span — their "turn" | one loop round trip — their "turn" (max_turns) | one model-node execution (before_model → after_model) |
| L3 | Event — half a step, the wire unit | network (OSI L3) — the packet | that span's start / end | that span's start / end | AssistantMessage out; tool results ride the next UserMessage | the two moments around the chat model's invoke() |
| L2 | Call — one tool call | data link (OSI L2) | execute_tool span | function_span | a tool_use block; PreToolUse is its gate | a ToolNode call; wrap_tool_call is its gate |
| L1 | Exec — one real execution | physical (OSI L1) | — | — | what Bash / Edit actually did on the host | what the tool function actually did |
| — | Agent (entity, off the stack) | host / endpoint | gen_ai.agent.id / .name | the Agent object (agent_span); a handoff switches it | the agent, and each subagent | the compiled graph |
| — | Workspace · Tenant | security zone · administrative boundary | (deployment.environment.name) | — | — | — |
The numbers line up through L4 on purpose. Exec/call/event/step sit on physical/link/network/transport, and the packet is L3 in both columns. Above transport the columns part: networking has only "application", because network applications share no structure — agent traffic is a dialogue with stable structure, so turn and session are this domain's own L5 and L6, not OSI's session and presentation layers (the two practice discarded).
⚠️ "Turn" means this stack's STEP in two of the three SDKs. In both the
OpenAI Agents SDK and the Claude Agent SDK a turn is one iteration of the
agent loop — one model call plus the tool runs it triggers — and that is what
max_turns counts. An OGR turn is the user-instruction episode that
contains those iterations: one Runner.run(), one query() prompt, one
graph invoke(). Same word, one layer apart. (The OpenAI Agents SDK
documentation uses both senses: max_turns counts loop iterations, while "a
single logical turn in a chat conversation" is one Runner.run() — an OGR
turn.)
Tracing spans
Three differences that are not vocabulary. They are why OGR does not simply consume spans:
- A span is an interval; a GuardEvent is a half. A span is written when its operation ends — after the model has answered, after the tool has run. OGR's two moments are the ones where something is still held and can still be refused: before the request reaches the model, and after the response arrives but before the agent acts on it. A span cannot block, so a step is two events rather than one record. (An SDK hook can refuse — see the sections below. A span never can.)
- A span declares its coordinates; a GuardEvent declares one.
trace_id,span_idandparent_span_idare producer-authored — and a producer that can declare a flow can get the flow wrong. The wire keepsstep_idalone, because pairing the two halves of one model call under concurrency is the single fact a runtime cannot derive. Session, turn and step numbering are derived server-side. - Telemetry is best-effort and sampled; enforcement is neither. Dropping
spans is normal operation; a dropped guard event is an unjudged model call.
The same asymmetry governs content: message capture is opt-in for a tracer
(
gen_ai.input.messages/gen_ai.output.messages), mandatory here, because the content is the event.
A fourth difference is shape. A trace is an open-ended tree — any framework nests whatever spans it likes — while this stack is six fixed layers, so the same traffic from two different harnesses lands on the same coordinates.
If your harness already emits spans, three mappings are directly useful when you write the integration:
- Mint
step_idfrom the inference span'sspan_id. One span covers both halves of the step — exactly the pairing rule — and it makes every guard row joinable to the trace it came from. - Send
gen_ai.conversation.idassession_hint. - Send
gen_ai.agent.id/gen_ai.agent.name/user.idas the identity four-tuple'sagent_id/agent_type/agent_user, and name your instrumentation inintegrationthe way an OTel instrumentation scope names itself.
What does not carry over: exporting spans to a collector is not an
integration. The evaluate call is synchronous and sits in the byte path —
see Instrument your agent.
In OpenInference, span kind AGENT ≈ turn, LLM ≈ step, TOOL ≈ call,
session.id ≈ session, and user.id ≈ agent_user; its GUARDRAIL kind is
where an OGR evaluate call itself would appear, if you traced it.
OpenAI Agents SDK
- Session — a
Session(SQLiteSession("user_123")) is the conversation the runner prepends before a run and appends to after it. Send its id assession_hint; a trace'sgroup_id, which links the traces of one chat thread, carries the same fact and is equally good. - Turn — one
Runner.run()/run_sync()/run_streamed(), which the SDK also wraps in one trace. ItsRunResultis the turn's outcome. - Step — one iteration of the runner's loop. The
generation_span(chat completions) orresponse_span(Responses API) is 1:1 with the model call: mintstep_idfrom its id. A customModel/ModelProvideris the natural enforcement point, because it holds the request before it is sent and the response before the runner acts on the tool calls. - Call — a
function_span. Aguardrail_spanis where theevaluatecall appears if you trace it — though note the SDK's own input/output guardrails run beside the model call, while an OGR decision sits in it. - Handoff —
handoff_spanhas no layer, because a handoff is movement on the entity axis, not a unit of traffic: the steps after it carry a differentagent_idinside the same turn and the same session.agent_spanis that agent's slice of the run — and an agent is an endpoint, not a layer.
Claude Agent SDK
- Session — the SDK's own session:
session_idoff the initSystemMessageor theResultMessage,resumeto return to it,forkto branch it. Send it assession_hint. This SDK produces both of the cases the hint exists for: compaction (compact_boundary) rewrites the history, so the conversation prefix a runtime chains on vanishes mid-conversation, while fork does the opposite — two live sessions sharing a long identical prefix. Content alone re-attaches the first wrongly and merges the second. The hint settles both. - Turn — one
query()prompt, up to itsResultMessage(withClaudeSDKClient, oneclient.query()call). - Step — one round trip of the loop, which this SDK calls a turn. The
AssistantMessageis the response half — text, thinking andtool_useblocks together, one generation, one event — and theUserMessagecarrying tool results belongs to the next request half. That is exactly the rule the protocol states: a call's result is judged in the followingstep/request. - Call — a
tool_useblock, gated by thePreToolUsehook. That hook is an enforcement point in the OGR sense: it can reject a call and hand the model a rejection instead. It is where the Claude Code plugin sits. - Exec — what
Bash,WriteorEditactually did on the host. Named by the model, not carried by the contract. - Subagents — a subagent runs its own conversation with fresh context and
returns only its final response to the parent, as a tool result. Its traffic
is its own session: give it its own
session_hint, and keep the sameagent_idunless you want it inventoried as a separate agent.
LangGraph
- Session — the thread:
configurable.thread_id, persisted by a checkpointer. That id is thesession_hint; the checkpointer is the local analogue of the runtime's session table. - Turn — one
invoke()/stream()on the compiled graph for that thread. - Step — one execution of the model node:
create_agent'sbefore_model→after_modelwindow, or the chat model'sinvoke()inside the prebuilt ReAct agent. Not a super-step — that is graph-execution granularity: nodes running in parallel share one, and a node that calls no model produces no events at all. This stack observes the model plane, not the graph. - Call — a
ToolNodeexecution, gated bywrap_tool_call. - State —
AgentState.messages(theadd_messagesreducer) is the conversation the request payload already carries; nothing about state needs sending separately. - Interrupt —
interrupt()/HumanInTheLoopMiddlewareis the structural twin of ablock: the graph pauses before a consequential call. The difference is who answers — a person there, a policy decision point here. - The LangGraph integration wraps the chat model for exactly this reason: the model node holds both refusable moments, so every graph built on that model is covered with no per-node work.
What none of them have
Everything above the agent. An SDK models one process; workspace (one
security zone, one policy set) and tenant (the administrative boundary,
carried by the API key and never by the payload) exist because what gets
governed is a fleet, not a run. The nearest neighbor a tracing vocabulary
offers is the resource attribute deployment.environment.name, and it is not
a policy boundary.
Why six layers — and why not OSI's seven
OGR follows the pragmatic TCP/IP cut, not OSI's seven: a layer earns its place with its own unit, its own mechanism, and its own question. Above transport, networking has only "application", because network applications share no structure — but agent traffic is a dialogue with stable structure, so turn and session are this domain's own layers, defined here rather than mapped onto OSI's vestigial session/presentation layers. And the agent stays off the stack for the same reason a host is not a protocol layer.
The firewall vocabulary carries over with the method:
| Firewall / network | OGR |
|---|---|
| packet | event |
| 5-tuple | the identity four-tuple + the API key's tenant |
| fragment reassembly | step_id pairing |
| session table, idle aging | server-side session state, idle timeout |
| stateful inspection | session / turn derivation |
| deep packet inspection | detection over the payload's texts |
| pass / drop | verdict allow / block |
| security zone | workspace — one zone, one policy set |
Normative text
The layer model is normative since OGR v1.0:
specification/overview.md
§ The layer model.