---
url: 'https://adk.nht.io/the-loop/primitives/message.md'
description: >-
  One unit of dialogue with a typed role, a Tokenizable body, a required
  Identity, and an attachments slot for Media.
---

# Message

[Primitives](../primitives) covers the eight-primitive overview.

A [`Message`](https://adk.nht.io/api/@nhtio/adk/common/classes/Message) is one unit of dialogue, attributed to a speaker, shaped for the model's next read. It carries two roles: `'user'` and `'assistant'`. Those are the only two things a `Message` can be.

::: danger If it isn't dialogue, it isn't a Message
There is no `'system'` role. There is no `'tool'` role. Every other category of content an agent produces lives in its own primitive, carrying the fields an executor needs to render it as its own tier:

* **System prompt / standing instructions** → [`TurnContext.systemPrompt`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-systemprompt) / [`TurnContext.standingInstructions`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-standinginstructions) on the [`TurnContext`](../turn-runner#turncontext).
* **Tool result** → [`ToolCall.results`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-results).
* **Reasoning** → [`Thought`](./thought).
* **Retrieved document, web result, KB chunk** → [`Retrievable`](./retrievable).
* **Durable fact to recall across turns** → [`Memory`](./memory).

A tool result rendered as a `'user'` turn inherits user authority. A retrieved doc rendered as an `'assistant'` turn inherits assistant authority. Neither of those is authority the content has earned — and granting it is the exact privilege escalation the [trust-tier rendering](../trust-tiers) pattern exists to prevent. The ADK refuses the extra roles so an executor that opts into that pattern has the data it needs to keep the tiers separate.
:::

::: tip Nothing stops you from doing it anyway
You can put whatever you want in a `Message` — that's your choice. But ADK doesn't treat them all as equal, because neither will the LLM you're interacting with. The routing above is the version of this that holds up under contact with real models.
:::
::: danger Don't be a hero
Shoving tool results or RAG context into 'user' roles nukes the trust boundary and turns your data into a privilege escalation vector. The renderer stops knowing what's a command and what's context, leaving the LLM to guess who's in charge. You can bypass the schema, but you're just building a prompt injection playground.
:::
`identity` is the load-bearing field for telling speakers apart in real turns — group chats, support escalations, planner-and-executor pipelines, specialist routing. The model has to distinguish voices, and your system has to correlate them with real users. The schema makes it *practically* required by defaulting to `role` when omitted (so an unset `identity` collapses to a single-participant `'user'` or `'assistant'` identity), and a bare string is accepted at construction as a single-participant convenience — the constructor builds an [`Identity`](./identity) whose `identifier` and `representation` are both that string. Either shortcut stops being correct the moment a second voice enters the loop; pass an explicit `Identity` once there is more than one participant per role.

A `Message` carries `content` (text), [`Message.attachments`](https://adk.nht.io/api/@nhtio/adk/common/classes/Message#property-attachments) ([`Media`](./media) — images, audio, video, documents — described in the next section), or both. It carries at least one: the cross-field rule on `rawMessageSchema` enforces that, and a message with neither throws `E_INVALID_INITIAL_MESSAGE_VALUE`. Attachments are symmetric across roles: both `user` messages (a human dropping in a screenshot with a question) and `assistant` messages (a model returning generated audio or an image) may carry them. Each attachment carries its own [`Media.trustTier`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-trusttier) and [`Media.modalityHazard`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-modalityhazard), and the renderer wraps each one in its own trust envelope independent of the message envelope — a `user` message envelope (`<message from="…">`) does not contaminate the attachment's tier, and vice versa. How a battery orders text vs attachments in the on-the-wire content array is a renderer-policy concern, not a contract of `Message`; the OpenAI Chat Completions battery emits text first, then attachments in array order.

::: tip Copy this mental model

* `Message.content` = text the model reads as dialogue.
* [`Message.attachments`](https://adk.nht.io/api/@nhtio/adk/common/classes/Message#property-attachments) = [`Media`](./media) bytes (each with its own trust envelope, independent of the message envelope).
* [`ToolCall.results`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-results) = tool outputs (artifacts *or* media), never a `Message`.

:::
