Skip to content

Design challenges with streaming content part deltas and message persistence. #383

@vlymar

Description

@vlymar

👋 Hello, first off I wanted to thank you for the awesome work you've been doing on this library.

I'm building an agent with a chat interface and have run into some design challenges. I'm hoping to address these either by working together to extend this library, or to get guidance on a workaround.

For context, we're building a chat assistant embedded in our application. For now I'm only testing with the Anthropic provider, and I'm building with stream: true set.

We've found that rendered messages in our chat UI map more to "content blocks" (anthropic api term) than they do to LangChain.Message objects. For example, here is a Message returned by on_message_processed:

%LangChain.Message{
  content: [
    %LangChain.Message.ContentPart{
      type: :thinking,
      content: "Here is my full thinking...",
      options: [ ... ]
    },
    %LangChain.Message.ContentPart{
      type: :text,
      content: "Here is my full response...",
      options: []
    }
  ],
  processed_content: nil,
  index: 1,
  status: :complete,
  role: :assistant,
  name: nil,
  tool_calls: [],
  tool_results: nil,
  metadata: %{
    usage: %LangChain.TokenUsage{ ... },
      cumulative: false
    }
  }
}

This Message has two content blocks. As the deltas are emitted, they first fire with index: 0 (for :thinking) then index: 1 (for :text).

Here are the design challenges:

  1. I need to represent content block boundaries in my agent's API response to our frontend. In Anthropic's API, these boundary events are "content_block_start" and "content_block_stop". OpenAI's new response API has the same notion. Vercel's AI SDK does as well. On the other hand, langchain's delta callback provides no indication of a content block start or finish. I've found that I can infer a content block boundary by sending delta events from a callback to a genserver, and checking for a change from one index to another, but this has issues:
    • Requires stateful aggregation of data in my langchain wrapper to reconstruct data already returned by the provider APIs (I've only verified that anthropic and openAI's Responses API return boundary events).
    • The final content block is the last index, so there's no index increment to detect after it. Can workaround by looking for a status: :completed delta or a on_message_processed callback fire, but that's additional complexity.
    • Depends on each content block being sent consecutively. Maybe this is a safe assumption but I'm not sure.

What do you think about introducing a new field that could be used to detect the end of a content block? I think if langchain announces the end of a content block, my wrapper can infer the start of a new block as well. Perhaps this could be ContentPart.status which is marked as :complete when the provider emits its form of content_block_stop?

  1. I need to persist agent messages so that a) historical chats can be viewed and b) chat history can be loaded as context into multi-turn conversations. I haven't quite figured out how best to do this. I'm conflicted between storing content parts as individual rows (which maps well to my agent's API and our chat interface), vs storing an entire representation of a Langchain.Message together. Regardless, I believe this ticket is related as either way it'd be helpful to have a stable identifier for messages that I could use to address DB rows. But here I'm less confident and more looking for any guidance you have to offer.

I apologize for the length of this ticket. Happy to clarify anything, or split it up into two tickets if that'd help!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions