@livekit/agents 1.0.35 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/voice/agent_activity.cjs +19 -19
  2. package/dist/voice/agent_activity.cjs.map +1 -1
  3. package/dist/voice/agent_activity.d.ts.map +1 -1
  4. package/dist/voice/agent_activity.js +19 -19
  5. package/dist/voice/agent_activity.js.map +1 -1
  6. package/dist/voice/agent_session.cjs +64 -25
  7. package/dist/voice/agent_session.cjs.map +1 -1
  8. package/dist/voice/agent_session.d.cts +25 -1
  9. package/dist/voice/agent_session.d.ts +25 -1
  10. package/dist/voice/agent_session.d.ts.map +1 -1
  11. package/dist/voice/agent_session.js +64 -25
  12. package/dist/voice/agent_session.js.map +1 -1
  13. package/dist/voice/index.cjs +14 -1
  14. package/dist/voice/index.cjs.map +1 -1
  15. package/dist/voice/index.d.cts +1 -0
  16. package/dist/voice/index.d.ts +1 -0
  17. package/dist/voice/index.d.ts.map +1 -1
  18. package/dist/voice/index.js +3 -1
  19. package/dist/voice/index.js.map +1 -1
  20. package/dist/voice/room_io/room_io.cjs +1 -0
  21. package/dist/voice/room_io/room_io.cjs.map +1 -1
  22. package/dist/voice/room_io/room_io.d.ts.map +1 -1
  23. package/dist/voice/room_io/room_io.js +1 -0
  24. package/dist/voice/room_io/room_io.js.map +1 -1
  25. package/dist/voice/speech_handle.cjs +12 -3
  26. package/dist/voice/speech_handle.cjs.map +1 -1
  27. package/dist/voice/speech_handle.d.cts +12 -2
  28. package/dist/voice/speech_handle.d.ts +12 -2
  29. package/dist/voice/speech_handle.d.ts.map +1 -1
  30. package/dist/voice/speech_handle.js +10 -2
  31. package/dist/voice/speech_handle.js.map +1 -1
  32. package/dist/voice/testing/index.cjs +52 -0
  33. package/dist/voice/testing/index.cjs.map +1 -0
  34. package/dist/voice/testing/index.d.cts +20 -0
  35. package/dist/voice/testing/index.d.ts +20 -0
  36. package/dist/voice/testing/index.d.ts.map +1 -0
  37. package/dist/voice/testing/index.js +31 -0
  38. package/dist/voice/testing/index.js.map +1 -0
  39. package/dist/voice/testing/run_result.cjs +477 -0
  40. package/dist/voice/testing/run_result.cjs.map +1 -0
  41. package/dist/voice/testing/run_result.d.cts +226 -0
  42. package/dist/voice/testing/run_result.d.ts +226 -0
  43. package/dist/voice/testing/run_result.d.ts.map +1 -0
  44. package/dist/voice/testing/run_result.js +451 -0
  45. package/dist/voice/testing/run_result.js.map +1 -0
  46. package/dist/voice/testing/types.cjs +46 -0
  47. package/dist/voice/testing/types.cjs.map +1 -0
  48. package/dist/voice/testing/types.d.cts +83 -0
  49. package/dist/voice/testing/types.d.ts +83 -0
  50. package/dist/voice/testing/types.d.ts.map +1 -0
  51. package/dist/voice/testing/types.js +19 -0
  52. package/dist/voice/testing/types.js.map +1 -0
  53. package/package.json +3 -3
  54. package/src/voice/agent_activity.ts +24 -22
  55. package/src/voice/agent_session.ts +73 -28
  56. package/src/voice/index.ts +1 -0
  57. package/src/voice/room_io/room_io.ts +1 -0
  58. package/src/voice/speech_handle.ts +24 -4
  59. package/src/voice/testing/index.ts +49 -0
  60. package/src/voice/testing/run_result.ts +576 -0
  61. package/src/voice/testing/types.ts +118 -0
@@ -0,0 +1,576 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type { AgentHandoffItem, ChatItem } from '../../llm/chat_context.js';
5
+ import type { Task } from '../../utils.js';
6
+ import { Future } from '../../utils.js';
7
+ import type { Agent } from '../agent.js';
8
+ import { type SpeechHandle, isSpeechHandle } from '../speech_handle.js';
9
+ import {
10
+ type AgentHandoffAssertOptions,
11
+ type AgentHandoffEvent,
12
+ type ChatMessageEvent,
13
+ type EventType,
14
+ type FunctionCallAssertOptions,
15
+ type FunctionCallEvent,
16
+ type FunctionCallOutputAssertOptions,
17
+ type FunctionCallOutputEvent,
18
+ type MessageAssertOptions,
19
+ type RunEvent,
20
+ isAgentHandoffEvent,
21
+ isChatMessageEvent,
22
+ isFunctionCallEvent,
23
+ isFunctionCallOutputEvent,
24
+ } from './types.js';
25
+
26
+ // Environment variable for verbose output
27
+ const evalsVerbose = parseInt(process.env.LIVEKIT_EVALS_VERBOSE || '0', 10);
28
+
29
+ /**
30
+ * Result of a test run containing recorded events and assertion utilities.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const result = await session.run({ userInput: 'Hello' });
35
+ * result.expect.nextEvent().isMessage({ role: 'assistant' });
36
+ * result.expect.noMoreEvents();
37
+ * ```
38
+ */
39
+ export class RunResult<T = unknown> {
40
+ private _events: RunEvent[] = [];
41
+ private doneFut = new Future<void>();
42
+ private userInput?: string;
43
+
44
+ private handles: Set<SpeechHandle | Task<void>> = new Set();
45
+ private lastSpeechHandle?: SpeechHandle;
46
+ private runAssert?: RunAssert;
47
+
48
+ // TODO(brian): Add typed output support for parity with Python
49
+ // - Add outputType?: new (...args: unknown[]) => T
50
+ // - Add finalOutput?: T
51
+ // - Implement markDone() to extract final_output from SpeechHandle.maybeRunFinalOutput
52
+ // - See Python: run_result.py lines 182-201
53
+
54
+ constructor(options?: { userInput?: string }) {
55
+ this.userInput = options?.userInput;
56
+ }
57
+
58
+ /**
59
+ * List of all recorded events generated during the run.
60
+ */
61
+ get events(): RunEvent[] {
62
+ return this._events;
63
+ }
64
+
65
+ /**
66
+ * Provides an assertion helper for verifying the run events.
67
+ */
68
+ get expect(): RunAssert {
69
+ if (evalsVerbose) {
70
+ const eventsStr = formatEvents(this._events)
71
+ .map((line) => ` ${line}`)
72
+ .join('\n');
73
+ console.log(
74
+ `\n+ RunResult {\n userInput: "${this.userInput}"\n events: [\n${eventsStr}\n ]\n }`,
75
+ );
76
+ }
77
+
78
+ // Cache the RunAssert so cursor position persists across multiple .expect accesses
79
+ if (!this.runAssert) {
80
+ this.runAssert = new RunAssert(this);
81
+ }
82
+ return this.runAssert;
83
+ }
84
+
85
+ /**
86
+ * Returns the final output of the run after completion.
87
+ *
88
+ * @throws Error - Not implemented yet.
89
+ */
90
+ get finalOutput(): T {
91
+ // TODO(brian): Implement typed output support after AgentTask is implemented.
92
+ throw new Error('finalOutput is not yet implemented in JS.');
93
+ }
94
+
95
+ /**
96
+ * Indicates whether the run has finished processing all events.
97
+ */
98
+ done(): boolean {
99
+ return this.doneFut.done;
100
+ }
101
+
102
+ /**
103
+ * Wait for the RunResult to complete. Returns `this` for method chaining.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const result = session.run({ userInput: 'Hi!' });
108
+ * await result.wait(); // waits for completion
109
+ * result.expect.nextEvent().isMessage({ role: 'assistant' });
110
+ * ```
111
+ */
112
+ async wait(): Promise<this> {
113
+ await this.doneFut.await;
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * @internal
119
+ * Records an agent handoff event.
120
+ */
121
+ _agentHandoff(params: { item: AgentHandoffItem; oldAgent?: Agent; newAgent: Agent }): void {
122
+ const event: AgentHandoffEvent = {
123
+ type: 'agent_handoff',
124
+ item: params.item,
125
+ oldAgent: params.oldAgent,
126
+ newAgent: params.newAgent,
127
+ };
128
+ const index = this._findInsertionIndex(event.item.createdAt);
129
+ this._events.splice(index, 0, event);
130
+ }
131
+
132
+ /**
133
+ * @internal
134
+ * Called when a chat item is added during the run.
135
+ */
136
+ _itemAdded(item: ChatItem): void {
137
+ if (this.doneFut.done) {
138
+ return;
139
+ }
140
+
141
+ let event: RunEvent | undefined;
142
+
143
+ if (item.type === 'message') {
144
+ event = { type: 'message', item } as ChatMessageEvent;
145
+ } else if (item.type === 'function_call') {
146
+ event = { type: 'function_call', item } as FunctionCallEvent;
147
+ } else if (item.type === 'function_call_output') {
148
+ event = { type: 'function_call_output', item } as FunctionCallOutputEvent;
149
+ }
150
+
151
+ if (event) {
152
+ const index = this._findInsertionIndex(item.createdAt);
153
+ this._events.splice(index, 0, event);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @internal
159
+ * Watch a speech handle or task for completion.
160
+ */
161
+ _watchHandle(handle: SpeechHandle | Task<void>): void {
162
+ this.handles.add(handle);
163
+
164
+ if (isSpeechHandle(handle)) {
165
+ handle._addItemAddedCallback(this._itemAdded.bind(this));
166
+ }
167
+
168
+ handle.addDoneCallback(() => {
169
+ this._markDoneIfNeeded(handle);
170
+ });
171
+ }
172
+
173
+ /**
174
+ * @internal
175
+ * Unwatch a handle.
176
+ */
177
+ _unwatchHandle(handle: SpeechHandle | Task<void>): void {
178
+ this.handles.delete(handle);
179
+
180
+ if (isSpeechHandle(handle)) {
181
+ handle._removeItemAddedCallback(this._itemAdded.bind(this));
182
+ }
183
+ }
184
+
185
+ private _markDoneIfNeeded(handle: SpeechHandle | Task<void>): void {
186
+ if (isSpeechHandle(handle)) {
187
+ this.lastSpeechHandle = handle;
188
+ }
189
+
190
+ if ([...this.handles].every((h) => (isSpeechHandle(h) ? h.done() : h.done))) {
191
+ this._markDone();
192
+ }
193
+ }
194
+
195
+ private _markDone(): void {
196
+ // TODO(brian): Implement final output support after AgentTask is implemented.
197
+ // See Python run_result.py _mark_done() for reference:
198
+ // - Check lastSpeechHandle._maybeRunFinalOutput
199
+ // - Validate output type matches expected type
200
+ // - Set exception or resolve based on output
201
+ if (!this.doneFut.done) {
202
+ this.doneFut.resolve();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Find the correct insertion index to maintain chronological order.
208
+ */
209
+ private _findInsertionIndex(createdAt: number): number {
210
+ for (let i = this._events.length - 1; i >= 0; i--) {
211
+ if (this._events[i]!.item.createdAt <= createdAt) {
212
+ return i + 1;
213
+ }
214
+ }
215
+ return 0;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Assertion helper for verifying run events in sequence.
221
+ */
222
+ export class RunAssert {
223
+ private _events: RunEvent[];
224
+ private _currentIndex = 0;
225
+
226
+ // TODO(brian): Add range access for parity with Python __getitem__ slice support.
227
+ // - Add range(start?, end?) method returning EventRangeAssert
228
+ // - EventRangeAssert should have containsFunctionCall(), containsMessage() methods
229
+ // See Python run_result.py lines 247-251 for reference.
230
+
231
+ constructor(runResult: RunResult) {
232
+ this._events = runResult.events;
233
+ }
234
+
235
+ /**
236
+ * Access a specific event by index for assertions.
237
+ * Supports negative indices (e.g., -1 for last event).
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * result.expect.at(0).isMessage({ role: 'user' });
242
+ * result.expect.at(-1).isMessage({ role: 'assistant' });
243
+ * ```
244
+ */
245
+ at(index: number): EventAssert {
246
+ let normalizedIndex = index;
247
+ if (index < 0) {
248
+ normalizedIndex = this._events.length + index;
249
+ }
250
+
251
+ if (normalizedIndex < 0 || normalizedIndex >= this._events.length) {
252
+ this._raiseWithDebugInfo(
253
+ `at(${index}) out of range (total events: ${this._events.length})`,
254
+ normalizedIndex,
255
+ );
256
+ }
257
+
258
+ return new EventAssert(this._events[normalizedIndex]!, this, normalizedIndex);
259
+ }
260
+
261
+ /**
262
+ * Advance to the next event, optionally filtering by type.
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * result.expect.nextEvent().isMessage({ role: 'assistant' });
267
+ * result.expect.nextEvent({ type: 'function_call' }).isFunctionCall({ name: 'foo' });
268
+ * ```
269
+ */
270
+ nextEvent(options?: { type?: EventType }): EventAssert {
271
+ while (true) {
272
+ const evAssert = this._currentEvent();
273
+ this._currentIndex++;
274
+
275
+ if (!options?.type || evAssert.event().type === options.type) {
276
+ return evAssert;
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Skip a specified number of upcoming events without assertions.
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * result.expect.skipNext(2);
287
+ * ```
288
+ */
289
+ skipNext(count: number = 1): this {
290
+ for (let i = 0; i < count; i++) {
291
+ if (this._currentIndex >= this._events.length) {
292
+ this._raiseWithDebugInfo(`Tried to skip ${count} event(s), but only ${i} were available.`);
293
+ }
294
+ this._currentIndex++;
295
+ }
296
+ return this;
297
+ }
298
+
299
+ /**
300
+ * Assert that there are no further events.
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * result.expect.noMoreEvents();
305
+ * ```
306
+ */
307
+ noMoreEvents(): void {
308
+ if (this._currentIndex < this._events.length) {
309
+ const event = this._events[this._currentIndex]!;
310
+ this._raiseWithDebugInfo(`Expected no more events, but found: ${event.type}`);
311
+ }
312
+ }
313
+
314
+ private _currentEvent(): EventAssert {
315
+ if (this._currentIndex >= this._events.length) {
316
+ this._raiseWithDebugInfo('Expected another event, but none left.');
317
+ }
318
+ return this.at(this._currentIndex);
319
+ }
320
+
321
+ /** @internal */
322
+ _raiseWithDebugInfo(message: string, index?: number): never {
323
+ const markerIndex = index ?? this._currentIndex;
324
+ const eventsStr = formatEvents(this._events, markerIndex).join('\n');
325
+ throw new AssertionError(`${message}\nContext around failure:\n${eventsStr}`);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Assertion wrapper for a single event.
331
+ */
332
+ export class EventAssert {
333
+ protected _event: RunEvent;
334
+ protected _parent: RunAssert;
335
+ protected _index: number;
336
+
337
+ constructor(event: RunEvent, parent: RunAssert, index: number) {
338
+ this._event = event;
339
+ this._parent = parent;
340
+ this._index = index;
341
+ }
342
+
343
+ /**
344
+ * Get the underlying event.
345
+ */
346
+ event(): RunEvent {
347
+ return this._event;
348
+ }
349
+
350
+ protected _raise(message: string): never {
351
+ this._parent._raiseWithDebugInfo(message, this._index);
352
+ }
353
+
354
+ /**
355
+ * Verify this event is a message with optional role matching.
356
+ *
357
+ * @example
358
+ * ```typescript
359
+ * result.expect.nextEvent().isMessage({ role: 'assistant' });
360
+ * ```
361
+ */
362
+ isMessage(options?: MessageAssertOptions): MessageAssert {
363
+ if (!isChatMessageEvent(this._event)) {
364
+ this._raise(`Expected ChatMessageEvent, got ${this._event.type}`);
365
+ }
366
+
367
+ if (options?.role && this._event.item.role !== options.role) {
368
+ this._raise(`Expected role '${options.role}', got '${this._event.item.role}'`);
369
+ }
370
+
371
+ return new MessageAssert(this._event, this._parent, this._index);
372
+ }
373
+
374
+ /**
375
+ * Verify this event is a function call with optional name/args matching.
376
+ *
377
+ * @example
378
+ * ```typescript
379
+ * result.expect.nextEvent().isFunctionCall({ name: 'order_item', args: { id: 'big_mac' } });
380
+ * ```
381
+ */
382
+ isFunctionCall(options?: FunctionCallAssertOptions): FunctionCallAssert {
383
+ if (!isFunctionCallEvent(this._event)) {
384
+ this._raise(`Expected FunctionCallEvent, got ${this._event.type}`);
385
+ }
386
+
387
+ if (options?.name && this._event.item.name !== options.name) {
388
+ this._raise(`Expected call name '${options.name}', got '${this._event.item.name}'`);
389
+ }
390
+
391
+ if (options?.args) {
392
+ let actual: Record<string, unknown>;
393
+ try {
394
+ actual = JSON.parse(this._event.item.args);
395
+ } catch {
396
+ this._raise(`Failed to parse function call arguments: ${this._event.item.args}`);
397
+ }
398
+
399
+ for (const [key, value] of Object.entries(options.args)) {
400
+ if (!(key in actual) || actual[key] !== value) {
401
+ this._raise(
402
+ `For key '${key}', expected ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`,
403
+ );
404
+ }
405
+ }
406
+ }
407
+
408
+ return new FunctionCallAssert(this._event, this._parent, this._index);
409
+ }
410
+
411
+ /**
412
+ * Verify this event is a function call output with optional matching.
413
+ *
414
+ * @example
415
+ * ```typescript
416
+ * result.expect.nextEvent().isFunctionCallOutput({ isError: false });
417
+ * ```
418
+ */
419
+ isFunctionCallOutput(options?: FunctionCallOutputAssertOptions): FunctionCallOutputAssert {
420
+ if (!isFunctionCallOutputEvent(this._event)) {
421
+ this._raise(`Expected FunctionCallOutputEvent, got ${this._event.type}`);
422
+ }
423
+
424
+ if (options?.output !== undefined && this._event.item.output !== options.output) {
425
+ this._raise(`Expected output '${options.output}', got '${this._event.item.output}'`);
426
+ }
427
+
428
+ if (options?.isError !== undefined && this._event.item.isError !== options.isError) {
429
+ this._raise(`Expected isError=${options.isError}, got ${this._event.item.isError}`);
430
+ }
431
+
432
+ return new FunctionCallOutputAssert(this._event, this._parent, this._index);
433
+ }
434
+
435
+ /**
436
+ * Verify this event is an agent handoff with optional type matching.
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * result.expect.nextEvent().isAgentHandoff({ newAgentType: MyAgent });
441
+ * ```
442
+ */
443
+ isAgentHandoff(options?: AgentHandoffAssertOptions): AgentHandoffAssert {
444
+ if (!isAgentHandoffEvent(this._event)) {
445
+ this._raise(`Expected AgentHandoffEvent, got ${this._event.type}`);
446
+ }
447
+
448
+ // Cast to the correct type after validation
449
+ const event = this._event as AgentHandoffEvent;
450
+
451
+ if (options?.newAgentType) {
452
+ const actualType = event.newAgent.constructor.name;
453
+ if (!(event.newAgent instanceof options.newAgentType)) {
454
+ this._raise(`Expected new_agent '${options.newAgentType.name}', got '${actualType}'`);
455
+ }
456
+ }
457
+
458
+ return new AgentHandoffAssert(event, this._parent, this._index);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Assertion wrapper for message events.
464
+ */
465
+ export class MessageAssert extends EventAssert {
466
+ protected declare _event: ChatMessageEvent;
467
+
468
+ constructor(event: ChatMessageEvent, parent: RunAssert, index: number) {
469
+ super(event, parent, index);
470
+ }
471
+
472
+ override event(): ChatMessageEvent {
473
+ return this._event;
474
+ }
475
+
476
+ // Phase 3: judge() method will be added here
477
+ }
478
+
479
+ /**
480
+ * Assertion wrapper for function call events.
481
+ */
482
+ export class FunctionCallAssert extends EventAssert {
483
+ protected declare _event: FunctionCallEvent;
484
+
485
+ constructor(event: FunctionCallEvent, parent: RunAssert, index: number) {
486
+ super(event, parent, index);
487
+ }
488
+
489
+ override event(): FunctionCallEvent {
490
+ return this._event;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Assertion wrapper for function call output events.
496
+ */
497
+ export class FunctionCallOutputAssert extends EventAssert {
498
+ protected declare _event: FunctionCallOutputEvent;
499
+
500
+ constructor(event: FunctionCallOutputEvent, parent: RunAssert, index: number) {
501
+ super(event, parent, index);
502
+ }
503
+
504
+ override event(): FunctionCallOutputEvent {
505
+ return this._event;
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Assertion wrapper for agent handoff events.
511
+ */
512
+ export class AgentHandoffAssert extends EventAssert {
513
+ protected declare _event: AgentHandoffEvent;
514
+
515
+ constructor(event: AgentHandoffEvent, parent: RunAssert, index: number) {
516
+ super(event, parent, index);
517
+ }
518
+
519
+ override event(): AgentHandoffEvent {
520
+ return this._event;
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Custom assertion error for test failures.
526
+ */
527
+ export class AssertionError extends Error {
528
+ constructor(message: string) {
529
+ super(message);
530
+ this.name = 'AssertionError';
531
+ Error.captureStackTrace?.(this, AssertionError);
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Format events for debug output, optionally marking a selected index.
537
+ */
538
+ function formatEvents(events: RunEvent[], selectedIndex?: number): string[] {
539
+ const lines: string[] = [];
540
+
541
+ for (let i = 0; i < events.length; i++) {
542
+ const event = events[i]!;
543
+ let prefix = '';
544
+ if (selectedIndex !== undefined) {
545
+ prefix = i === selectedIndex ? '>>>' : ' ';
546
+ }
547
+
548
+ let line: string;
549
+ if (isChatMessageEvent(event)) {
550
+ const { role, content, interrupted } = event.item;
551
+ const textContent =
552
+ typeof content === 'string'
553
+ ? content
554
+ : Array.isArray(content)
555
+ ? content.filter((c): c is string => typeof c === 'string').join(' ')
556
+ : '';
557
+ const truncated = textContent.length > 50 ? textContent.slice(0, 50) + '...' : textContent;
558
+ line = `${prefix}[${i}] { type: "message", role: "${role}", content: "${truncated}", interrupted: ${interrupted} }`;
559
+ } else if (isFunctionCallEvent(event)) {
560
+ const { name, args } = event.item;
561
+ line = `${prefix}[${i}] { type: "function_call", name: "${name}", args: ${args} }`;
562
+ } else if (isFunctionCallOutputEvent(event)) {
563
+ const { output, isError } = event.item;
564
+ const truncated = output.length > 50 ? output.slice(0, 50) + '...' : output;
565
+ line = `${prefix}[${i}] { type: "function_call_output", output: "${truncated}", isError: ${isError} }`;
566
+ } else if (isAgentHandoffEvent(event)) {
567
+ line = `${prefix}[${i}] { type: "agent_handoff", oldAgent: "${event.oldAgent?.constructor.name}", newAgent: "${event.newAgent.constructor.name}" }`;
568
+ } else {
569
+ line = `${prefix}[${i}] ${event}`;
570
+ }
571
+
572
+ lines.push(line);
573
+ }
574
+
575
+ return lines;
576
+ }
@@ -0,0 +1,118 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type {
5
+ AgentHandoffItem,
6
+ ChatMessage,
7
+ ChatRole,
8
+ FunctionCall,
9
+ FunctionCallOutput,
10
+ } from '../../llm/chat_context.js';
11
+ import type { Agent } from '../agent.js';
12
+
13
+ /**
14
+ * Event representing an assistant or user message in the conversation.
15
+ */
16
+ export interface ChatMessageEvent {
17
+ type: 'message';
18
+ item: ChatMessage;
19
+ }
20
+
21
+ /**
22
+ * Event representing a function/tool call initiated by the LLM.
23
+ */
24
+ export interface FunctionCallEvent {
25
+ type: 'function_call';
26
+ item: FunctionCall;
27
+ }
28
+
29
+ /**
30
+ * Event representing the output/result of a function call.
31
+ */
32
+ export interface FunctionCallOutputEvent {
33
+ type: 'function_call_output';
34
+ item: FunctionCallOutput;
35
+ }
36
+
37
+ /**
38
+ * Event representing an agent handoff (switching from one agent to another).
39
+ */
40
+ export interface AgentHandoffEvent {
41
+ type: 'agent_handoff';
42
+ item: AgentHandoffItem;
43
+ oldAgent?: Agent;
44
+ newAgent: Agent;
45
+ }
46
+
47
+ /**
48
+ * Union type of all possible run events that can occur during a test run.
49
+ */
50
+ export type RunEvent =
51
+ | ChatMessageEvent
52
+ | FunctionCallEvent
53
+ | FunctionCallOutputEvent
54
+ | AgentHandoffEvent;
55
+
56
+ /**
57
+ * Type guard to check if an event is a ChatMessageEvent.
58
+ */
59
+ export function isChatMessageEvent(event: RunEvent): event is ChatMessageEvent {
60
+ return event.type === 'message';
61
+ }
62
+
63
+ /**
64
+ * Type guard to check if an event is a FunctionCallEvent.
65
+ */
66
+ export function isFunctionCallEvent(event: RunEvent): event is FunctionCallEvent {
67
+ return event.type === 'function_call';
68
+ }
69
+
70
+ /**
71
+ * Type guard to check if an event is a FunctionCallOutputEvent.
72
+ */
73
+ export function isFunctionCallOutputEvent(event: RunEvent): event is FunctionCallOutputEvent {
74
+ return event.type === 'function_call_output';
75
+ }
76
+
77
+ /**
78
+ * Type guard to check if an event is an AgentHandoffEvent.
79
+ */
80
+ export function isAgentHandoffEvent(event: RunEvent): event is AgentHandoffEvent {
81
+ return event.type === 'agent_handoff';
82
+ }
83
+
84
+ /**
85
+ * Options for message assertion.
86
+ */
87
+ export interface MessageAssertOptions {
88
+ role?: ChatRole;
89
+ }
90
+
91
+ /**
92
+ * Options for function call assertion.
93
+ */
94
+ export interface FunctionCallAssertOptions {
95
+ name?: string;
96
+ args?: Record<string, unknown>;
97
+ }
98
+
99
+ /**
100
+ * Options for function call output assertion.
101
+ */
102
+ export interface FunctionCallOutputAssertOptions {
103
+ output?: string;
104
+ isError?: boolean;
105
+ }
106
+
107
+ /**
108
+ * Options for agent handoff assertion.
109
+ */
110
+ export interface AgentHandoffAssertOptions {
111
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
112
+ newAgentType?: new (...args: any[]) => Agent;
113
+ }
114
+
115
+ /**
116
+ * Event type literals for type-safe event filtering.
117
+ */
118
+ export type EventType = 'message' | 'function_call' | 'function_call_output' | 'agent_handoff';