Skip to content

Conversation

@thomasyuill-livekit
Copy link
Contributor

@thomasyuill-livekit thomasyuill-livekit commented Nov 7, 2025

Migrate to LiveKit React Agent SDK

This PR integrates the LiveKit React Agent SDK (@livekit/components-react@0.0.0-20251021151641), replacing custom implementations with SDK-provided hooks and providers.

Key Changes

Session Management

  • Replaced custom SessionProvider with SDK's SessionProvider + new AppSessionProvider wrapper
  • Added hooks/useAppSession.tsx to centralize session state and lifecycle management
  • Removed hooks/useRoom.ts (functionality moved to SDK)

Hooks & State

  • Replaced useChatMessages with SDK's useSessionMessages
  • Replaced useConnectionTimeout with new useAgentErrors for better error handling
  • Updated hook calls: useRoomContext() → useSessionContext(), useVoiceAssistant() → useAgent()

Type Updates

  • ReceivedChatMessage → ReceivedMessage
  • Removed editTimestamp/hasBeenEdited properties

Component Updates

  • ViewController and SessionView refactored to use SDK session management
  • AgentControlBar now receives isSessionActive as prop instead of from context
  • Improved session lifecycle timing with animation-aware start/end

Other

  • Updated sonner to ^2.0.7
  • Responsive alert toast width adjustments

@vercel
Copy link

vercel bot commented Nov 7, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
sandbox-voice-assistant Ready Ready Preview Nov 11, 2025 3:21pm

@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 5087f3f to 71fac98 Compare November 7, 2025 16:39
@thomasyuill-livekit thomasyuill-livekit marked this pull request as draft November 7, 2025 16:39
@thomasyuill-livekit thomasyuill-livekit requested review from 1egoman and lukasIO and removed request for lukasIO November 7, 2025 16:39
@thomasyuill-livekit thomasyuill-livekit self-assigned this Nov 7, 2025
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 71fac98 to 2b9eeb9 Compare November 7, 2025 16:49
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 2b9eeb9 to 7f77240 Compare November 7, 2025 16:51
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch 2 times, most recently from b595bdf to b6ab0a3 Compare November 7, 2025 16:55
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from b6ab0a3 to abca768 Compare November 7, 2025 16:59
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from abca768 to 37e53d8 Compare November 7, 2025 17:04
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 37e53d8 to 4d1da91 Compare November 7, 2025 17:26
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 4d1da91 to 33eba9e Compare November 7, 2025 18:02
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 33eba9e to 2a528dc Compare November 7, 2025 18:13
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 2a528dc to 554bbba Compare November 7, 2025 18:19
Comment on lines 74 to 96
const session = useSession(tokenSource, sessionOptions);

const value = useMemo(
() => ({
isSessionActive,
startSession: (startSession = true) => {
setIsSessionActive(true);
if (startSession) {
session.start();
}
},
endSession: (endSession = true) => {
setIsSessionActive(false);
if (endSession) {
session.end();
}
},
}),
// session object is not a stable reference
// TODO: add session object to dependencies
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[isSessionActive]
);
Copy link
Contributor Author

@thomasyuill-livekit thomasyuill-livekit Nov 7, 2025

Choose a reason for hiding this comment

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

@1egoman @lukasIO

Would love to get your thoughts on this,

I want control over the enter and exit transitions, delaying the ending of the session until the transition has complete, to prevent the video avatar disappearing before the exit transition has completed.

Also,
I'm noticing session isn't a stable reference (even if I pass in stable references) so to prevent it triggering context re-renders I've left it out of the dependency array

Copy link
Contributor

Choose a reason for hiding this comment

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

@thomasyuill-livekit

I want control over the enter and exit transitions, delaying the ending of the session until the transition has complete, to prevent the video avatar disappearing before the exit transition has completed.

I'm not sure that I understand where this is being used - can you point me to the call sites where startSession / endSession is being called with that boolean parameter? I did see this which looks like it might be related to the problem you are mentioning but it doesn't seem to be using this app session abstraction.

I'm noticing session isn't a stable reference (even if I pass in stable references) so to prevent it triggering context re-renders I've left it out of the dependency array

Huh, that is definitely not intentional - it should be. I'll look into this, thanks for surfacing it!

Copy link
Contributor

Choose a reason for hiding this comment

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

@thomasyuill-livekit FYI that I think I figured out the useSession return stability issue. I've got a pull request fixing it here: livekit/components-js#1230

Copy link
Contributor Author

@thomasyuill-livekit thomasyuill-livekit Nov 10, 2025

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

That link you posted above links me to a section of the code where I see startSession is being defined (const { isSessionActive, startSession } = useAppSession();) but not where it is used.

Scanning through the diff, I see it is used here, where it is being passed as onStartCall into MotionWelcomeView, but onStartCall isn't being called with parameters (from here) so I'm not sure how this boolean parameter is being used to conditionally call session.start() / session.end().

Am I missing something? Sorry if so, just want to make sure I'm properly able to map this out in my head.

Copy link
Contributor Author

@thomasyuill-livekit thomasyuill-livekit Nov 11, 2025

Choose a reason for hiding this comment

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

I believe the piece you're missing (onDisconnect) is found here

By default, startSession and endSession manage controlling the app state isSessionActive and the session state session.start()/session.end()

if you pass false into startSession or endSession, you are opting to control session state manually.

In session-view.tsx we create an handleDisconnect callback that opts out of the default behaviour, requiring the manual calling of session.end() that happens in the ViewContoller, after the exit transition here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added some additional comments to the useAppSession context value to document this behaviour

Copy link
Contributor

@1egoman 1egoman Nov 12, 2025

Choose a reason for hiding this comment

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

Ah I see! Thank you for that additional context.

I will rephrase my understanding of the problem, just to make sure I have it right - clicking disconnect should update the application's state so that an animation can occur, and once the animation completes then you want to call session.end() to actually end the session. This is important so that the livekit generated video streams that are active stay active throughout the duration of the animation.

Assuming I have that right - my initial take is this is going to be inevitably fairly nuanced given there's a series of steps that the system must progress through asynchronously. It's possible that the agent sdk could do more to help with this, and I'll throw a few ideas below but I'm not sure if the agent sdk should really have to care about this given it's a problem fairly outside of its responsibility. I could be convinced though, because I could imagine this would be a fairly common rough edge.

A few potential options that come to mind in the collapsed section below:

1. Continue to push handling this down to downstream client applications

IMO if this is what is decided, I think the thing that makes what you currently have fairly confusing to me at least is that startSession / endSession currently do multiple things. I think breaking this up into two methods, making it clear one should be called "when animation starts" and one called "when animation ends" would help a lot.

2. Some sort of "keep the session running while this is rendered" component

Update the agents sdk to add some sort of component that can be rendered to keep the session "stuck" open while the animation is running. Then when react-motion or a similar tool finished the animation and unmounts the dom, the component cleans up some effect, which causes the session to really end.

Rough example:

const session = useSession(/* ... */);

// Somewhere one or multiple of these could be rendered (ie, alongside the avatar):
<KeepSessionConnected session={session} />

// Then later on:
const promise = session.end();
// session.status is now "disconnecting"
// ( ... animation occurs ... )
// session.status is STILL "disconnecting"
// Finally, the `KeepSessionConnected` component is unmounted
// session.status is STILL "disconnected"
// `promise` resolves

Important considerations:

  • There probably would need to be a new disconnecting state or something similar that can be used to make it clear that the disconnect is now async. That being said, this probably should have existed already since room.disconnect is inherently async.
  • How does this interact with the failed / other future non-disconnect terminal states? ie, would disconnecting after a session has failed also go to disconnecting or would this behavior only apply for happy path scenarios? IMO, I think it should always go through disconnecting (and maybe that is just not a great name for what this actually is).

3. Some sort of "before end" type event

Update the agents sdk to add some sort of async callback to session.end, which could return a promise that could allow you to wait for the animation to complete before continuing. So something like:

const session = useSession(/* ... */);

// Later on:
// session.state is "connected"
session.end(async () => {
  // session.state is "disconnecting"
  // wait for animation and resolve once complete
});
// session.state is "disconnected"

Important considerations: Same as 2

Copy link
Contributor

Choose a reason for hiding this comment

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

Discussed this with @lukasIO out of band - it sounds like he's also of a similar mind that this isn't a responsibility that the agents sdk should take on.

I think (at least to me) what makes the current abstractions hard to reason about is that the animation state is so coupled with the session state. Is there anything you can do to decouple this from useSession's state?

Also, I think the name AppSession is sufficiently close to Session to also make things confusing. If you need to keep this context around, maybe there's something else you could call it to make it clear that it's not necessarily directly related to the existing useSession hook / SessionProvider component.

@thomasyuill-livekit thomasyuill-livekit changed the title chore: migrate to useSession hook chore: migrate to Agents SDK (useSession hook) Nov 7, 2025
1egoman added a commit to livekit/client-sdk-js that referenced this pull request Nov 10, 2025
This function is needed for use in `components-js` so I can fix an issue
reported by Thom
[here](livekit-examples/agent-starter-react#298 (comment))
related to the useSession return not being a stable reference due to
fetch options changing every render.
@thomasyuill-livekit thomasyuill-livekit changed the title chore: migrate to Agents SDK (useSession hook) feat: migrate to Agents SDK (useSession hook) Nov 10, 2025
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 554bbba to 70d9de0 Compare November 10, 2025 15:39
@thomasyuill-livekit thomasyuill-livekit marked this pull request as ready for review November 10, 2025 17:04
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 7d0f187 to 5061173 Compare November 11, 2025 15:20
Comment on lines 76 to 78
messageOrigin={messageOrigin}
hasBeenEdited={hasBeenEdited}
{...MESSAGE_MOTION_PROPS}
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: is it worth continuing to pass through this hasBeenEdited prop when the ReceivedMessage is the subtype of ReceivedChatMessage?

key="welcome"
{...VIEW_MOTION_PROPS}
startButtonText={appConfig.startButtonText}
startButtonText={appConfig?.startButtonText ?? ''}
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: is appConfig ever able to be unset? It looks like in the props for the component appConfig should always be passed so I think this is redundant can can be just made into a regular property access?

Suggested change
startButtonText={appConfig?.startButtonText ?? ''}
startButtonText={appConfig.startButtonText}

});
}

return TokenSource.endpoint('/api/connection-details');
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, I'm glad you were able to make /api/connection-details work with the endpoint abstraction!

Comment on lines +69 to +72
const sessionOptions = useMemo(
() => (appConfig.agentName ? { agentName: appConfig.agentName } : undefined),
[appConfig]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: Just a FYI that this useMemo shouldn't be required once this is merged, which I plan on doing shortly. I'll post an update here once it is merged.

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.

3 participants