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/[email protected]), 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 Canceled Canceled Nov 13, 2025 3:47pm

@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 108
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

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.

Copy link
Contributor Author

@thomasyuill-livekit thomasyuill-livekit Nov 13, 2025

Choose a reason for hiding this comment

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

I agree it shouldn't be the responsibility of the SDK, this is the concern of the application and it's views.
On the surface it doesn't seem that complicated, fade out the view, then end the session, but the exectution does seem to require some indirection.

I've explored your suggestion on the client side to decouple the animation state and session state but I found the code harder to manage and reason about as there are twice as many conditions and states to check.

I've renamed useAppSession and the related artifacts to useConnection, though I do feel that introduces it's own opportunities for confusion as it's just a set of utils for managing the app's agent session and corresponding view states.

I'm currently exploring a path where you can call 'deferDisconnect` to opt out of the default behaviour and receive a callback to end the session manually, but I'm personally not finding it to be more intuitive, just a different method of indirection. I've got a few rough edges to work out so I haven't committed anything yet

This just seems to be one of the complexities that arises from the presentation layer when creating a more polished UX.

Copy link
Contributor Author

@thomasyuill-livekit thomasyuill-livekit Nov 13, 2025

Choose a reason for hiding this comment

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

After sleeping on it, I've scrapped the deferred disconnect approach.

I'm going with the more explicit approach you suggested.

I have 2 disconnect functions,

  • startDisconnect which starts the transitions
  • onDisconnectTransitionComplete that handles ending the session

I feel this makes the dance steps explicit, and clearer to follow

@thomasyuill-livekit thomasyuill-livekit changed the title chore: migrate to useSession hook chore: migrate to Agents SDK (useSession hook) Nov 7, 2025
@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?

Copy link
Contributor Author

@thomasyuill-livekit thomasyuill-livekit Nov 13, 2025

Choose a reason for hiding this comment

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

because this is an app scoped component, and we've updated the dependencies,
I don't feel we need to support the old type because it will never be true because we're now using useSessionMessages

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have enough context on this to have a super strong opinion, so I'll leave it up to you.

The thing I just want to make sure that is clear though is a message of type ReceivedMessage can sometimes have a editTimestamp parameter if it falls under one of its discriminated union branches - ie, message.type === 'chatMessage' ? message.editTimestamp : null typechecks properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

useSessionMessages appears to returns the ReceivedMessage subtype and not ReceivedChatMessage because it handles the array composition and transform we previously did in the app

@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 5061173 to 5020567 Compare November 12, 2025 22:00
@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 f85a658 to d435989 Compare November 13, 2025 15:32
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from d435989 to 7e18aae Compare November 13, 2025 15:36
@thomasyuill-livekit thomasyuill-livekit force-pushed the thomasyuill/clt-2150-integrate-broader-react-agent-sdk-usesession-etc-into-agent branch from 7e18aae to 6d823c7 Compare November 13, 2025 15:41
@thomasyuill-livekit
Copy link
Contributor Author

@1egoman @lukasIO

This PR has the latest dependencies and addresses the latest feedback
LMKWYT

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