Skip to content

Add synthetic display message hook for middleware#88

Merged
brainlid merged 1 commit intomainfrom
me-support-synthetic-messages
May 4, 2026
Merged

Add synthetic display message hook for middleware#88
brainlid merged 1 commit intomainfrom
me-support-synthetic-messages

Conversation

@brainlid
Copy link
Copy Markdown
Contributor

@brainlid brainlid commented May 4, 2026

Problem

Middleware sometimes needs to record user-facing transcript entries that don't correspond to a LangChain.Message from the LLM. Two concrete cases:

  • An ask_user middleware needs the user's answer to appear in the conversation as a user message, even though it never went through the model as a user message (it's a tool response).
  • A cancellation flow needs a system-style notification like "User cancelled" to be visible in the transcript on reload.

There was no supported path for middleware to persist these entries through the same display-message pipeline that LLM-generated messages use, and therefore no way to get them broadcast to LiveViews via the existing :display_message_saved event.

Solution

Add a public AgentServer API that middleware can call to persist a synthetic display message, plus an optional callback on the DisplayMessagePersistence behaviour that integrators implement to actually write it.

  • Sagents.AgentServer.save_synthetic_message_from/2 casts to the AgentServer, which routes the request to the configured persistence module and broadcasts the saved record as {:display_message_saved, msg} — the same event LiveViews already subscribe to for LLM messages.
  • The new save_synthetic_message/3 callback is declared optional on the behaviour, so existing implementations don't break. AgentServer checks function_exported?/3 before calling and logs a warning if missing.
  • Persistence failures ({:error, _} or raised exceptions) are logged but never propagate — synthetic messages are a transcript convenience and must not destabilise the agent.
  • Generator template (priv/templates/display_message_persistence.ex.eex) gets a default implementation that delegates to the integrator's Conversations context, so newly generated projects pick this up automatically.

Changes

  • lib/sagents/agent_server.ex — Added save_synthetic_message_from/2 public API, :save_synthetic_message cast handler, and the maybe_save_synthetic_and_broadcast/2 helper that gates on persistence + conversation_id, checks the optional callback, and rescues errors.
  • lib/sagents/display_message_persistence.ex — Added synthetic_message_attrs typedoc and save_synthetic_message/3 optional callback with full @doc covering parameters, return shape, and the broadcast contract.
  • priv/templates/display_message_persistence.ex.eex — Default generated implementation delegating to Conversations.append_display_message/3, with a {:error, :no_conversation} guard for missing conversation_id.
  • test/sagents/agent_server_synthetic_message_test.exs — New test module covering the happy path, both no-op cases (missing conversation_id, missing persistence module), {:error, _} returns, and rescued exceptions.
  • test/support/test_display_message_persistence.ex — Extended TestDisplayMessagePersistenceForwarding with save_synthetic_message/3 plus set_synthetic_response/1 and clear_synthetic_response/0 helpers driven via :persistent_term, so a single test module can simulate ok/error/raise outcomes.

Testing

mix test test/sagents/agent_server_synthetic_message_test.exs covers all five scenarios listed above. No live API calls — the forwarder persistence module substitutes for a real implementation and asserts the AgentServer's behaviour around it.

- can be created by middleware
- like ask_user responses being displayed as a user message in conversation
- also system "notification" style messages like "User cancelled"
@brainlid brainlid merged commit 207e9e8 into main May 4, 2026
2 checks passed
@brainlid brainlid deleted the me-support-synthetic-messages branch May 4, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant