Skip to content

feat(voice): add public add_conversation_item method to AgentSession - #7099

Open
wardandr wants to merge 3 commits into
livekit:mainfrom
wardandr:feat/add-conversation-item-public-api
Open

feat(voice): add public add_conversation_item method to AgentSession#7099
wardandr wants to merge 3 commits into
livekit:mainfrom
wardandr:feat/add-conversation-item-public-api

Conversation

@wardandr

@wardandr wardandr commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Adds AgentSession.add_conversation_item(item: ChatMessage) -> bool β€” a public, async method to insert a ChatMessage into the session history and the active agent's chat context, emitting conversation_item_added the same way the internal audio pipeline does. Works uniformly across waterfall (STT β†’ LLM β†’ TTS) and realtime pipelines.

  • Inserts into session.history (session-level ChatContext) and, when an agent is running, into its chat context
  • Emits conversation_item_added so event listeners (transcript capture, analytics, etc.) observe the item normally
  • Waterfall: mirrors the internal two-step pattern (agent ctx insert + session history + event)
  • Realtime: push-then-commit β€” the item is pushed to the live model context via _rt_session.update_chat_ctx, and only a successful push commits locally. A failed push raises RealtimeError and leaves the session untouched, so retrying with the same item id works
  • Realtime models without a mutable chat context raise RealtimeError before any mutation
  • Idempotent by message ID β€” duplicate IDs are detected via ChatContext.get_by_id and silently skipped (returns False)
  • Items added before session.start() reach session history and the event only; they are never backfilled into the agent's context once it starts

Use cases

Current workaround this replaces

# Private API β€” mirrors agent_activity.py internals
self._agent._chat_ctx.items.append(user_message)
self._session._conversation_item_added(user_message)

New public API

msg = ChatMessage(role="user", content=["collected via DTMF"])
added = await session.add_conversation_item(msg)  # True if added, False if deduped

Closes #7085

Test plan

  • test_add_conversation_item_appears_in_history β€” message retrievable from session.history
  • test_add_conversation_item_emits_event β€” conversation_item_added event fires with correct payload
  • test_add_conversation_item_visible_to_agent β€” message appears in agent.chat_ctx
  • test_add_conversation_item_dedup_by_id β€” second call with same ID returns False; no duplicate event or agent-ctx entry
  • test_add_conversation_item_before_start β€” history + event fire pre-start; pins that the item is never backfilled into the agent's context
  • test_add_conversation_item_realtime_pushes_to_rt_session β€” item reaches the live realtime session context, local history, agent ctx, and event
  • test_add_conversation_item_realtime_non_mutable_raises β€” raises RealtimeError with zero side effects
  • test_add_conversation_item_realtime_push_failure_leaves_state_clean β€” failed push mutates nothing; retry with same id succeeds
  • test_add_conversation_item_realtime_dedup_by_id β€” dedup applies on the realtime path
  • Full unit suite: no regressions vs main; lint, format, and type-check (livekit.agents) pass

@wardandr
wardandr requested a review from a team as a code owner September 2, 2026 16:42
@CLAassistant

CLAassistant commented Sep 2, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

devin-ai-integration[bot]

This comment was marked as resolved.

@wardandr
wardandr force-pushed the feat/add-conversation-item-public-api branch from e4cd7a1 to 653abf1 Compare September 2, 2026 17:35
devin-ai-integration[bot]

This comment was marked as resolved.

Add `AgentSession.add_conversation_item(item)` so external code can
insert a ChatMessage into both the session history and the active
agent's chat context, emitting `conversation_item_added` the same
way the internal audio pipeline does.

The call is idempotent: duplicate message IDs are detected via
`ChatContext.get_by_id` and silently skipped.

Closes livekit#7085
Make the method async and uniform across pipelines:

- waterfall: unchanged β€” agent context + session history + event
- realtime: push-then-commit via `_rt_session.update_chat_ctx`; only a
  successful push commits locally, so a failed push leaves the session
  untouched and retrying with the same item id works
- realtime models without a mutable chat context raise `RealtimeError`

Document that items added before `start()` are never backfilled into
the agent's context, and pin that contract in tests.

Adds realtime tests with FakeRealtimeModel: provider push, non-mutable
raise, push failure + retry, and realtime-path dedup.
@wardandr
wardandr force-pushed the feat/add-conversation-item-public-api branch from 653abf1 to cdfebce Compare September 2, 2026 17:45
…omicity

Serialize concurrent callers with a dedicated lock and hold the
activity lock across the provider await so agent handoffs cannot
split an addition across old and new contexts.

Track in-flight item IDs so realtime provider echoes that arrive
before update_chat_ctx returns are suppressed by _on_remote_item_added
rather than inserted as duplicate placeholders.

Add concurrency, handoff-atomicity, agent-level realtime override,
and echo-before-failure tests.
πŸ€– Generated with [Nori](https://noriagentic.com)

Co-Authored-By: Nori <contact@tilework.tech>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

1 flag not posted on this PR by your GitHub settings β€” view it in Devin Review. (Configure)

Devin Review

Comment on lines +1594 to +1596
if agent._chat_ctx.get_by_id(item.id) is None:
agent._chat_ctx.insert(item)
self._conversation_item_added(item)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Concurrent transcript duplicates conversation item

When a matching final transcript lands during update_chat_ctx, add_conversation_item commits the same ID again. History and listeners receive duplicates.

Prompt for agents
Make the realtime commit phase re-check session history after update_chat_ctx returns. If another pipeline path committed the same ID during the await, avoid inserting or emitting it again and return the idempotent result. Keep the agent context, session history, and documented return value coherent under this race.
Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This race is already prevented. The method holds _activity_lock across the entire transaction (dedup check β†’ provider push β†’ local commit). The pipeline's _user_turn_completed_task path acquires the same lock before committing a transcript, so the two paths are serialized β€” a matching final transcript cannot land and commit while add_conversation_item is mid-flight.

Comment on lines +1590 to +1596
self._locally_pushed_conversation_item_ids.add(item.id)
try:
await rt_session.update_chat_ctx(candidate)

if agent._chat_ctx.get_by_id(item.id) is None:
agent._chat_ctx.insert(item)
self._conversation_item_added(item)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”΄ Fallback swaps drop added items

During a fallback swap, update_chat_ctx reports success without sending the item. add_conversation_item commits locally, but the replacement model never receives it.

Prompt for agents
Coordinate add_conversation_item with _FallbackRealtimeSession swaps. The fallback adapter currently drops update_chat_ctx calls while _swapping because ordinary callers already changed the agent context, but add_conversation_item uses push-then-commit and has not changed it yet. Ensure the item is included in the swap replay or make the update wait/retry against the replacement child before committing local state.
Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pre-existing limitation of _FallbackRealtimeSession's update_chat_ctx contract β€” it silently drops calls during a swap. That affects every caller of update_chat_ctx, not just add_conversation_item. Fixing the fallback adapter's swap semantics is out of scope for this PR; it would require changes to the _FallbackRealtimeSession internals.

Comment on lines +1592 to +1597
await rt_session.update_chat_ctx(candidate)

if agent._chat_ctx.get_by_id(item.id) is None:
agent._chat_ctx.insert(item)
self._conversation_item_added(item)
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Rejected updates appear successfully added

When update_chat_ctx absorbs a provider rejection, add_conversation_item returns True and commits locally. The live model never sees the item.

Prompt for agents
Define a provider-level acknowledgement contract that lets add_conversation_item determine whether its specific item was accepted. OpenAI update_chat_ctx currently logs per-item rejections and returns normally. Do not commit or return True unless the requested item reached the provider; otherwise raise RealtimeError and retain retry semantics.
Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above β€” this is a pre-existing contract issue with the RealtimeSession interface. OpenAI's update_chat_ctx absorbs per-item rejections and returns normally, which affects all callers. Defining a provider-level acknowledgement contract would require changes to the RealtimeSession base class, which is out of scope here. add_conversation_item follows the same contract as every other internal caller of update_chat_ctx.

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.

feat: public API to append a user-role conversation item and emit conversation_item_added

2 participants