Copy page
View as Markdown View this page as plain text

Messages

Each thread owns an ordered, persistent list of messages — the conversation history the agent reasons over. Tools, hooks, and endpoints read, inject, queue, and update these messages through ThreadState.

This page documents the message record, the APIs for manipulating the list, and delivery semantics for queued messages. Thread identity, execution state, and everything else about threads live on the Threads parent page.

Note: The APIs on this page mutate persisted message storage. To modify, inject, or filter messages non-destructively — only for the current model call, with the stored history untouched — use Hooks, specifically filter_messages, prefilter_llm_history, before_create_message, and before_update_message.

1. Message Structure

PropertyTypeDescription
idstringUnique message ID
role'system' | 'user' | 'assistant' | 'tool'Message role
contentstring | nullMessage content
namestring | nullTool name (for tool results)
tool_callsstring | nullJSON-encoded tool calls
tool_call_idstring | nullTool call ID (for results)
created_atnumberCreation timestamp
parent_idstring | nullParent message (sub-prompts)
depthnumberNesting depth (0 = top-level)
silentbooleanClient-presentation flag. Ordinary UI lists hide the message. The message remains eligible for model context and request-context hooks.
metadataRecord<string, unknown>Custom metadata (may include runtime status markers such as status_kind)
subagent_idstring | nullSubagent reference UUID associated with the message
subagent_relationshipstring | nullProjected relationship name (as) of the child (when available)
subagent_namestring | nullProjected child agent name (when available)
subagent_titlestring | nullProjected child agent title (when available)
subagent_descriptionstring | nullProjected child description (when available)
subagent_statusstring | nullProjected child status (when available)
subagent_blockingboolean | nullProjected blocking flag (when available)
subagent_thread_namestring | nullProjected human-friendly child instance name (when available)
subagent_spawn_group_idstring | nullProjected spawn grouping identifier (when available)

1.1 Provenance Metadata Conventions

Runtimes SHOULD set the following OPTIONAL metadata keys on messages whose content crossed a thread boundary, so relays and orchestrators need no ad-hoc bracket-tag prefixes:

KeyMeaning
metadata.subagent_idThe child instance reference this message relates to
metadata.forwarded_fromThe origin thread or message of forwarded content

These are conventions, not requirements: consumers MUST tolerate their absence, and runtimes MUST NOT rely on them for routing correctness.

2. Reading Messages

// Get recent messages
const { messages, total, hasMore } = await state.getMessages({
  limit: 50,
  order: 'desc',
});

// Get a single message
const message = await state.getMessage('msg-123');

2.1 Query Options

OptionTypeDefaultDescription
limitnumber-Max messages to return
offsetnumber0Messages to skip
order'asc' | 'desc''desc'Sort order
includeSilentbooleanfalseInclude silent messages in this query. This option affects read APIs only.
maxDepthnumber-Max nesting depth

3. Injecting Messages

Messages can be injected into the conversation:

const message = await state.injectMessage({
  role: 'user',
  content: 'Additional context...',
  silent: true, // Hide from ordinary UI lists
  metadata: { source: 'tool' },
});

3.1 Persistence and lifecycle

injectMessage() creates a persisted conversation message. The same contract applies during active execution and at rest.

  • Implementations MUST invoke before_create_message and after_create_message for every successful injection. See Hooks §4.
  • The returned promise MUST resolve after the message is persisted and available through thread message APIs (getMessage, getMessages).
  • injectMessage() MUST NOT recursively restart request-context hooks (filter_messages, prefilter_llm_history).

3.2 Presentation (silent)

silent is a client-presentation flag.

  • Clients MUST hide silent messages from ordinary message lists unless the caller sets includeSilent: true.
  • Silent messages MUST remain eligible for model context.
  • Silent messages MUST participate in the ordinary request-context hooks.
  • silent MUST NOT change persistence, lifecycle hooks, or same-execution visibility.

3.3 Same-execution visibility

During an active execution, a successfully injected message MUST become observable to subsequent tools and hooks in that execution.

execution.messageHistory MUST represent live chronological persisted execution history. It MUST include silent messages. Synthetic or transformed messages returned by request-context hooks MUST remain confined to the request context.

3.4 Context snapshot assignment

Each model-context assembly takes a stable snapshot of eligible persisted history. See Runtime §4 and Hooks §3.4.

  • Messages persisted before the snapshot MUST be eligible for that assembly.
  • Messages injected after the snapshot begins MUST become eligible for the following context assembly.

4. Queueing Messages

Messages can be queued for delivery on the next execution step:

await state.queueMessage({
  role: 'user', // the side B perspective
  content: 'Follow-up task',
  silent: true,
});

Perspective mapping (canonical statement). Message roles are the single source of perspective truth, for queueing, injection, and rendering alike:

  • role: 'user' is the side B perspective — a message side A acts on.
  • role: 'assistant' is the side A perspective — a message side B acts on.

This mapping is independent of what backs each side (human, prompt, or handler). Subagent traffic delivered to a parent lands in the perspective that prompts the relationship’s owning side to take its turn — see Subagents §2.1.

Queue semantics:

  • If the thread is executing, queued messages are injected before the next LLM request.
  • If the thread is idle, queueing forces a new turn.
  • Queued messages are processed in order.

5. Updating Messages

Existing messages can be updated:

const updated = await state.updateMessage('msg-123', {
  content: 'Updated content',
  metadata: { edited: true },
});

6. Deleting Messages

const deleted = await state.deleteMessage('msg-123');

Returns true if the message was found and removed, false otherwise. Tool-result messages cascade with their parent assistant message.

7. Conformance Scenarios

A conforming runtime MUST satisfy these portable scenarios.

ScenarioObservable result
Inject during active executionbefore_create_message and after_create_message each run once. getMessage returns the persisted record. Later tools and hooks in the same execution observe the message on execution.messageHistory.
Inject at restThe same lifecycle hooks run. The message is available through thread message APIs before the next execution starts.
Silent and visible injectionBoth records persist. Both appear on execution.messageHistory. Both are eligible for the next context snapshot. Ordinary UI queries omit the silent record unless includeSilent is true.
filter_messages edits the request contextTrimming, compaction, redaction, and synthetic messages affect the provider request. execution.messageHistory continues to list the live persisted records.
Inject from filter_messagesLifecycle hooks run. Context assembly does not restart. The injected message is absent from the current provider request and eligible for the following assembly.
Inject from prefilter_llm_historySame snapshot assignment as injection from filter_messages.
Inject from after_system_messageThe message is persisted before the history snapshot. It is eligible for the current assembly and passes through filter_messages.