Skip to content

feat: add durable identifiers to messages#2836

Draft
opieter-aws wants to merge 1 commit into
strands-agents:mainfrom
opieter-aws:opieter-aws/issue-2805-plan
Draft

feat: add durable identifiers to messages#2836
opieter-aws wants to merge 1 commit into
strands-agents:mainfrom
opieter-aws:opieter-aws/issue-2805-plan

Conversation

@opieter-aws

@opieter-aws opieter-aws commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Description

Messages do not carry a durable identity. The only per-message tracking available today is ephemeral: the memory ExtractionCoordinator's in-session high-water-mark sequence number, and SessionMessage.message_id — an ordinal index that is not stable across conversation-manager truncation or session restore. Neither survives as a durable key, so a memory store has no way to build a (session_id, message_id) tuple to deduplicate extracted messages across sessions.

This change gives every message a durable, stable id in both SDKs. The id is assigned once, when a message is added to the conversation, and is preserved everywhere that message is later observed — MessageAddedEvent subscribers, session persistence, and snapshots. It is never sent to model providers (the existing role/content whitelist already strips everything else). Memory stores can now combine this id with a session id to identify a message uniquely across restarts.

Assignment happens at append time rather than at construction. This keeps it idempotent — a message that already has an id (restored from a session, supplied by a caller, or re-appended) keeps it — which is what makes the id stable through a save/restore cycle. It also means messages that were persisted before this change are left without an id rather than being silently backfilled with a fresh one on each load.

Both SDKs generate a canonical (hyphenated) UUID v4: Python via str(uuid.uuid4()), TypeScript via crypto.randomUUID(). The shapes match deliberately, so a message id means the same thing regardless of which SDK produced it.

Wiring the id into the memory extraction pipeline (so stores receive it for deduplication) is intentionally out of scope here; this PR only establishes the durable id on the Message type so that work can build on it.

This is a coordinated change across both SDKs to keep the Message shape consistent. Python and TypeScript are kept behaviorally identical: assign at the append chokepoint, preserve on redaction, exclude from provider payloads, and no backfill of legacy messages.

Public API Changes

Message gains an optional, durable id.

class Message(TypedDict):
    """A message in a conversation with the agent.

    Attributes:
        content: The message content.
        role: The role of the message sender.
        id: Durable, stable identifier for the message, assigned when the message is added to the
            conversation. Survives session save/restore and snapshots, and is stripped before model
            calls. Combined with a session id, it gives memory stores a key to deduplicate messages
            across sessions.
        metadata: Optional metadata, stripped before model calls.
    """

    content: list[ContentBlock]
    role: Role
    id: NotRequired[str] <-- New!!
    metadata: NotRequired[MessageMetadata]

Python — new id field on the Message TypedDict, plus a null-safe accessor:

from strands.types.content import get_message_id

# After a turn, every recorded message carries a stable id
agent("Hello")
message = agent.messages[-1]
message["id"]            # e.g. "686b8abc-1db4-4145-aa60-9615c04f50ef"
get_message_id(message)  # same value, or None if never assigned

# The id survives a session save/restore round-trip
agent_2 = Agent(session_manager=FileSessionManager(session_id="s1", storage_dir=...), agent_id="a1")
agent_2.messages[-1]["id"]  # identical to the persisted id

TypeScript — new optional id on MessageData and the Message class; it round-trips through toJSON/fromJSON/clone:

await agent.invoke('Hello')
const message = agent.messages.at(-1)!
message.id  // e.g. "0f8e1c2d-...-..." (crypto.randomUUID), stable across serialization

Both fields are optional and backward compatible: existing code that constructs messages without an id is unaffected, and messages persisted before this change deserialize with no id. The id is assigned by the agent when a message is appended, so a caller-supplied id (e.g. on input messages) is always preserved.

Related Issues

Resolves: #2805

Documentation PR

No documentation changes. The id is assigned automatically and is primarily a building block for upcoming memory-store deduplication; there is no new user-facing workflow to document yet.

Type of Change

New feature

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce new warnings.

  • I ran hatch run prepare

Beyond the unit suites (both SDKs green), I exercised the change end to end: ran a real agent turn and confirmed every recorded message has a unique id, then persisted through a FileSessionManager and restored into a fresh agent, confirming the restored ids match the persisted ones. Regression tests assert the id never reaches the model-provider payload — in Python at the stream_messages whitelist, and in TypeScript at the Anthropic adapter's request formatting (where TS does its stripping). Note: hatch run prepare's static-analysis step couldn't bootstrap locally due to an unrelated native build failure in the optional [cedar] extra (rustc version); ruff/mypy on the changed files and the full test suites pass, and CI runs the gate cleanly.

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions github-actions Bot added size/l enhancement New feature or request area-sessions Related to session or session managment strands-running labels Jun 16, 2026
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@opieter-aws opieter-aws force-pushed the opieter-aws/issue-2805-plan branch from 83fae70 to fe16a56 Compare June 16, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-sessions Related to session or session managment enhancement New feature or request size/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add durable identifiers to messages

1 participant