Skip to content

Commit 11504b8

Browse files
committed
adjustments
1 parent 4f051bf commit 11504b8

File tree

4 files changed

+144
-29
lines changed

4 files changed

+144
-29
lines changed

lib/src/agent/agent.dart

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,51 @@ class Agent extends ChangeNotifier {
5555
RemoteVideoTrack? get avatarVideoTrack => _avatarVideoTrack;
5656
RemoteVideoTrack? _avatarVideoTrack;
5757

58-
/// Indicates whether the agent is connected.
59-
bool get isConnected => switch (_state) {
60-
_AgentLifecycle.connected => true,
61-
_AgentLifecycle.connecting => false,
62-
_AgentLifecycle.disconnected => false,
63-
_AgentLifecycle.failed => false,
64-
};
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+
}
6568

6669
/// Whether the agent is buffering audio prior to connecting.
6770
bool get isBuffering => _state == _AgentLifecycle.connecting && _isBuffering;
6871

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+
69103
_AgentLifecycle _state = _AgentLifecycle.disconnected;
70104
bool _isBuffering = false;
71105

@@ -179,11 +213,15 @@ class Agent extends ChangeNotifier {
179213
/// Describes why an [Agent] failed to connect.
180214
enum AgentFailure {
181215
/// The agent did not connect within the allotted timeout.
182-
timeout;
216+
timeout,
217+
218+
/// The agent left the room unexpectedly.
219+
left;
183220

184221
/// A human-readable error message.
185222
String get message => switch (this) {
186223
AgentFailure.timeout => 'Agent did not connect',
224+
AgentFailure.left => 'Agent left the room unexpectedly',
187225
};
188226
}
189227

lib/src/agent/session.dart

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import '../logger.dart';
2525
import '../managers/event.dart';
2626
import '../participant/remote.dart';
2727
import '../support/disposable.dart';
28+
import '../token_source/jwt.dart';
2829
import '../token_source/token_source.dart';
2930
import '../types/other.dart';
3031
import 'agent.dart';
@@ -177,36 +178,43 @@ class Session extends DisposableChangeNotifier {
177178

178179
final Duration timeout = _options.agentConnectTimeout;
179180

180-
Future<void> connect() async {
181+
Future<bool> connect() async {
181182
final response = await _tokenSourceConfiguration.fetch();
182183
await room.connect(
183184
response.serverUrl,
184185
response.participantToken,
185186
);
187+
return response.dispatchesAgent();
186188
}
187189

188190
try {
191+
final bool dispatchesAgent;
189192
if (_options.preConnectAudio) {
190-
await room.withPreConnectAudio(
193+
dispatchesAgent = await room.withPreConnectAudio(
191194
() async {
192195
_setConnectionState(ConnectionState.connecting);
193196
_agent.connecting(buffering: true);
194-
await connect();
197+
return connect();
195198
},
196199
timeout: timeout,
197200
);
198201
} else {
199202
_setConnectionState(ConnectionState.connecting);
200203
_agent.connecting(buffering: false);
201-
await connect();
204+
dispatchesAgent = await connect();
202205
await room.localParticipant?.setMicrophoneEnabled(true);
203206
}
204207

205-
_agentTimeoutTimer = Timer(timeout, () {
206-
if (isConnected && !_agent.isConnected) {
207-
_agent.failed(AgentFailure.timeout);
208-
}
209-
});
208+
if (dispatchesAgent) {
209+
_agentTimeoutTimer = Timer(timeout, () {
210+
if (isConnected && !_agent.isConnected) {
211+
_agent.failed(AgentFailure.timeout);
212+
}
213+
});
214+
} else {
215+
_agentTimeoutTimer?.cancel();
216+
_agentTimeoutTimer = null;
217+
}
210218
} catch (error, stackTrace) {
211219
logger.warning('Session.start() failed: $error', error, stackTrace);
212220
_setError(SessionError.connection(error));
@@ -300,27 +308,26 @@ class Session extends DisposableChangeNotifier {
300308
}
301309

302310
void _handleRoomEvent(RoomEvent event) {
311+
bool shouldUpdateAgent = false;
312+
303313
if (event is RoomConnectedEvent || event is RoomReconnectedEvent) {
304314
_setConnectionState(ConnectionState.connected);
315+
shouldUpdateAgent = true;
305316
} else if (event is RoomReconnectingEvent) {
306317
_setConnectionState(ConnectionState.reconnecting);
318+
shouldUpdateAgent = true;
307319
} else if (event is RoomDisconnectedEvent) {
308320
_setConnectionState(ConnectionState.disconnected);
309321
_agent.disconnected();
322+
shouldUpdateAgent = true;
323+
}
324+
325+
if (event is ParticipantEvent) {
326+
shouldUpdateAgent = true;
310327
}
311328

312-
switch (event) {
313-
case ParticipantConnectedEvent _:
314-
case ParticipantDisconnectedEvent _:
315-
case ParticipantAttributesChanged _:
316-
case TrackPublishedEvent _:
317-
case TrackUnpublishedEvent _:
318-
case RoomConnectedEvent _:
319-
case RoomReconnectedEvent _:
320-
case RoomReconnectingEvent _:
321-
_updateAgent();
322-
default:
323-
break;
329+
if (shouldUpdateAgent) {
330+
_updateAgent();
324331
}
325332
}
326333

@@ -336,6 +343,8 @@ class Session extends DisposableChangeNotifier {
336343
final RemoteParticipant? firstAgent = room.agentParticipants.firstOrNull;
337344
if (firstAgent != null) {
338345
_agent.connected(firstAgent);
346+
} else if (_agent.isConnected) {
347+
_agent.failed(AgentFailure.left);
339348
} else {
340349
_agent.connecting(buffering: _options.preConnectAudio);
341350
}

lib/src/token_source/jwt.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
1616
import 'package:json_annotation/json_annotation.dart';
1717

18+
import 'room_configuration.dart';
1819
import 'token_source.dart';
1920

2021
part 'jwt.g.dart';
@@ -75,6 +76,18 @@ class LiveKitJwtPayload {
7576
return null;
7677
}
7778

79+
/// Room configuration embedded in the token, if present.
80+
RoomConfiguration? get roomConfiguration {
81+
final raw = _claims['roomConfig'] ?? _claims['room_config'];
82+
if (raw is Map<String, dynamic>) {
83+
return RoomConfiguration.fromJson(Map<String, dynamic>.from(raw));
84+
}
85+
if (raw is Map) {
86+
return RoomConfiguration.fromJson(Map<String, dynamic>.from(raw));
87+
}
88+
return null;
89+
}
90+
7891
/// Token expiration instant in UTC.
7992
DateTime? get expiresAt => _dateTimeFor('exp');
8093

@@ -203,6 +216,12 @@ extension TokenSourceJwt on TokenSourceResponse {
203216

204217
return true;
205218
}
219+
220+
/// Returns `true` when the token's room configuration dispatches at least one agent.
221+
bool dispatchesAgent() {
222+
final agents = jwtPayload?.roomConfiguration?.agents;
223+
return agents != null && agents.isNotEmpty;
224+
}
206225
}
207226

208227
/// Extension to extract LiveKit-specific claims from JWT tokens.

test/token/token_source_test.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ void main() {
174174
'hidden': false,
175175
'recorder': true,
176176
},
177+
roomConfig: {
178+
'agents': [
179+
{
180+
'agent_name': 'demo-agent',
181+
'metadata': '{"foo":"bar"}',
182+
}
183+
]
184+
},
177185
);
178186

179187
final response = TokenSourceResponse(
@@ -201,6 +209,42 @@ void main() {
201209
expect(grant.canPublishSources, ['camera', 'screen']);
202210
expect(grant.hidden, isFalse);
203211
expect(grant.recorder, isTrue);
212+
213+
final config = payload.roomConfiguration;
214+
expect(config, isNotNull);
215+
expect(config!.agents, isNotNull);
216+
expect(config.agents, hasLength(1));
217+
expect(config.agents!.first.agentName, 'demo-agent');
218+
expect(config.agents!.first.metadata, '{"foo":"bar"}');
219+
});
220+
});
221+
222+
group('TokenSourceResponse', () {
223+
test('dispatchesAgent returns true when JWT config includes agents', () {
224+
final token = _generateToken(
225+
roomConfig: {
226+
'agents': [
227+
{'agent_name': 'assistant'}
228+
]
229+
},
230+
);
231+
232+
final response = TokenSourceResponse(
233+
serverUrl: 'https://test.livekit.io',
234+
participantToken: token,
235+
);
236+
237+
expect(response.dispatchesAgent(), isTrue);
238+
});
239+
240+
test('dispatchesAgent returns false when JWT lacks agents', () {
241+
final token = _generateToken();
242+
final response = TokenSourceResponse(
243+
serverUrl: 'https://test.livekit.io',
244+
participantToken: token,
245+
);
246+
247+
expect(response.dispatchesAgent(), isFalse);
204248
});
205249
});
206250

@@ -445,6 +489,7 @@ String _generateToken({
445489
String? metadata,
446490
Map<String, String>? attributes,
447491
Map<String, dynamic>? video,
492+
Map<String, dynamic>? roomConfig,
448493
}) {
449494
final payload = <String, dynamic>{
450495
'sub': subject ?? 'test-participant',
@@ -483,6 +528,10 @@ String _generateToken({
483528
payload['attributes'] = Map<String, String>.from(attributes);
484529
}
485530

531+
if (roomConfig != null) {
532+
payload['roomConfig'] = roomConfig;
533+
}
534+
486535
final jwt = JWT(payload);
487536
return jwt.sign(SecretKey('test-secret'));
488537
}

0 commit comments

Comments
 (0)