Skip to content
3 min read · 623 words

Message

Primitives covers the eight-primitive overview.

A 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.

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:

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 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.

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.

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 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 (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 and Media.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.

Copy this mental model

  • Message.content = text the model reads as dialogue.
  • Message.attachments = Media bytes (each with its own trust envelope, independent of the message envelope).
  • ToolCall.results = tool outputs (artifacts or media), never a Message.