Skip to content

Commit 24c2005

Browse files
authored
Session api (#909)
1 parent ceed1ab commit 24c2005

21 files changed

+1743
-119
lines changed

.changes/session-api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="added" "Session API"

build.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ targets:
1919
- example/**
2020
builders:
2121
json_serializable:
22+
generate_for:
23+
include:
24+
- lib/src/json/**.dart
25+
- lib/src/token_source/**.dart
2226
options:
2327
include_if_null: false
2428
explicit_to_json: true

lib/livekit_client.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ export 'src/livekit.dart';
2929
export 'src/logger.dart';
3030
export 'src/managers/event.dart';
3131
export 'src/options.dart';
32+
export 'src/agent/agent.dart';
33+
export 'src/agent/session.dart';
34+
export 'src/agent/session_options.dart';
35+
export 'src/agent/chat/message.dart';
36+
export 'src/agent/chat/message_sender.dart';
37+
export 'src/agent/chat/message_receiver.dart';
38+
export 'src/agent/chat/text_message_sender.dart';
39+
export 'src/agent/chat/transcription_stream_receiver.dart';
40+
export 'src/agent/room_agent.dart';
3241
export 'src/participant/local.dart';
3342
export 'src/participant/participant.dart';
3443
export 'src/participant/remote.dart' hide ParticipantCreationResult;
@@ -48,7 +57,7 @@ export 'src/track/remote/audio.dart';
4857
export 'src/track/remote/remote.dart';
4958
export 'src/track/remote/video.dart';
5059
export 'src/track/track.dart';
51-
export 'src/types/attribute_typings.dart';
60+
export 'src/json/agent_attributes.dart';
5261
export 'src/types/data_stream.dart';
5362
export 'src/types/other.dart';
5463
export 'src/types/participant_permissions.dart';

lib/src/agent/agent.dart

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
// Copyright 2025 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:flutter/foundation.dart';
16+
17+
import 'package:collection/collection.dart';
18+
19+
import '../json/agent_attributes.dart';
20+
import '../participant/participant.dart';
21+
import '../participant/remote.dart';
22+
import '../track/remote/audio.dart';
23+
import '../track/remote/video.dart';
24+
import '../types/other.dart';
25+
import 'constants.dart';
26+
27+
/// Represents a LiveKit Agent.
28+
///
29+
/// The [Agent] class models the state of a LiveKit agent within a [Session].
30+
/// It exposes information about the agent's connection status, conversational
31+
/// state, and the media tracks that belong to the agent. Consumers should
32+
/// observe [Agent] to update their UI when the agent connects, disconnects,
33+
/// or transitions between conversational states such as listening, thinking,
34+
/// and speaking.
35+
///
36+
/// The associated [Participant]'s attributes are inspected to derive the
37+
/// agent-specific metadata (such as [agentState]). Audio and avatar video
38+
/// tracks are picked from the agent participant and its associated avatar
39+
/// worker (if any).
40+
class Agent extends ChangeNotifier {
41+
Agent();
42+
43+
AgentFailure? get error => _error;
44+
AgentFailure? _error;
45+
46+
/// The current conversational state of the agent.
47+
AgentState? get agentState => _agentState;
48+
AgentState? _agentState;
49+
50+
/// The agent's audio track, if available.
51+
RemoteAudioTrack? get audioTrack => _audioTrack;
52+
RemoteAudioTrack? _audioTrack;
53+
54+
/// The agent's avatar video track, if available.
55+
RemoteVideoTrack? get avatarVideoTrack => _avatarVideoTrack;
56+
RemoteVideoTrack? _avatarVideoTrack;
57+
58+
/// Indicates whether the agent is connected and ready for conversation.
59+
bool get isConnected {
60+
if (_state != _AgentLifecycle.connected) {
61+
return false;
62+
}
63+
return switch (_agentState) {
64+
AgentState.LISTENING || AgentState.THINKING || AgentState.SPEAKING => true,
65+
_ => false,
66+
};
67+
}
68+
69+
/// Whether the agent is buffering audio prior to connecting.
70+
bool get isBuffering => _state == _AgentLifecycle.connecting && _isBuffering;
71+
72+
/// Whether the agent can currently listen for user input.
73+
bool get canListen {
74+
if (_state == _AgentLifecycle.connecting) {
75+
return _isBuffering;
76+
}
77+
if (_state == _AgentLifecycle.connected) {
78+
return switch (_agentState) {
79+
AgentState.LISTENING || AgentState.THINKING || AgentState.SPEAKING => true,
80+
_ => false,
81+
};
82+
}
83+
return false;
84+
}
85+
86+
/// Whether the agent is pending initialization.
87+
bool get isPending {
88+
if (_state == _AgentLifecycle.connecting) {
89+
return !_isBuffering;
90+
}
91+
if (_state == _AgentLifecycle.connected) {
92+
return switch (_agentState) {
93+
AgentState.IDLE || AgentState.INITIALIZING => true,
94+
_ => false,
95+
};
96+
}
97+
return false;
98+
}
99+
100+
/// Whether the agent finished or failed its session.
101+
bool get isFinished => _state == _AgentLifecycle.disconnected || _state == _AgentLifecycle.failed;
102+
103+
_AgentLifecycle _state = _AgentLifecycle.disconnected;
104+
bool _isBuffering = false;
105+
106+
/// Marks the agent as disconnected.
107+
void disconnected() {
108+
if (_state == _AgentLifecycle.disconnected &&
109+
_agentState == null &&
110+
_audioTrack == null &&
111+
_avatarVideoTrack == null &&
112+
_error == null) {
113+
return;
114+
}
115+
_state = _AgentLifecycle.disconnected;
116+
_isBuffering = false;
117+
_agentState = null;
118+
_audioTrack = null;
119+
_avatarVideoTrack = null;
120+
_error = null;
121+
notifyListeners();
122+
}
123+
124+
/// Marks the agent as connecting.
125+
void connecting({required bool buffering}) {
126+
_state = _AgentLifecycle.connecting;
127+
_isBuffering = buffering;
128+
_error = null;
129+
notifyListeners();
130+
}
131+
132+
/// Marks the agent as failed.
133+
void failed(AgentFailure failure) {
134+
_state = _AgentLifecycle.failed;
135+
_isBuffering = false;
136+
_error = failure;
137+
notifyListeners();
138+
}
139+
140+
/// Updates the agent with information from the connected [participant].
141+
void connected(RemoteParticipant participant) {
142+
final AgentState? nextAgentState = _readAgentState(participant);
143+
final RemoteAudioTrack? nextAudioTrack = _resolveAudioTrack(participant);
144+
final RemoteVideoTrack? nextAvatarTrack = _resolveAvatarVideoTrack(participant);
145+
146+
final bool shouldNotify = _state != _AgentLifecycle.connected ||
147+
_agentState != nextAgentState ||
148+
!identical(_audioTrack, nextAudioTrack) ||
149+
!identical(_avatarVideoTrack, nextAvatarTrack) ||
150+
_error != null ||
151+
_isBuffering;
152+
153+
_state = _AgentLifecycle.connected;
154+
_isBuffering = false;
155+
_error = null;
156+
_agentState = nextAgentState;
157+
_audioTrack = nextAudioTrack;
158+
_avatarVideoTrack = nextAvatarTrack;
159+
160+
if (shouldNotify) {
161+
notifyListeners();
162+
}
163+
}
164+
165+
AgentState? _readAgentState(Participant participant) {
166+
final rawState = participant.attributes[lkAgentStateAttributeKey];
167+
if (rawState == null) {
168+
return null;
169+
}
170+
switch (rawState) {
171+
case 'idle':
172+
return AgentState.IDLE;
173+
case 'initializing':
174+
return AgentState.INITIALIZING;
175+
case 'listening':
176+
return AgentState.LISTENING;
177+
case 'speaking':
178+
return AgentState.SPEAKING;
179+
case 'thinking':
180+
return AgentState.THINKING;
181+
default:
182+
return null;
183+
}
184+
}
185+
186+
RemoteAudioTrack? _resolveAudioTrack(RemoteParticipant participant) {
187+
final publication = participant.audioTrackPublications.firstWhereOrNull(
188+
(pub) => pub.source == TrackSource.microphone,
189+
);
190+
return publication?.track;
191+
}
192+
193+
RemoteVideoTrack? _resolveAvatarVideoTrack(RemoteParticipant participant) {
194+
final avatarWorker = _findAvatarWorker(participant);
195+
if (avatarWorker == null) {
196+
return null;
197+
}
198+
final publication = avatarWorker.videoTrackPublications.firstWhereOrNull(
199+
(pub) => pub.source == TrackSource.camera,
200+
);
201+
return publication?.track;
202+
}
203+
204+
RemoteParticipant? _findAvatarWorker(RemoteParticipant participant) {
205+
final publishOnBehalf = participant.identity;
206+
final room = participant.room;
207+
return room.remoteParticipants.values.firstWhereOrNull(
208+
(p) => p.attributes[lkPublishOnBehalfAttributeKey] == publishOnBehalf,
209+
);
210+
}
211+
}
212+
213+
/// Describes why an [Agent] failed to connect.
214+
enum AgentFailure {
215+
/// The agent did not connect within the allotted timeout.
216+
timeout,
217+
218+
/// The agent left the room unexpectedly.
219+
left;
220+
221+
/// A human-readable error message.
222+
String get message => switch (this) {
223+
AgentFailure.timeout => 'Agent did not connect',
224+
AgentFailure.left => 'Agent left the room unexpectedly',
225+
};
226+
}
227+
228+
enum _AgentLifecycle {
229+
disconnected,
230+
connecting,
231+
connected,
232+
failed,
233+
}

0 commit comments

Comments
 (0)