@livekit/agents 1.0.46 → 1.0.48
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.
- package/dist/beta/index.cjs +29 -0
- package/dist/beta/index.cjs.map +1 -0
- package/dist/beta/index.d.cts +2 -0
- package/dist/beta/index.d.ts +2 -0
- package/dist/beta/index.d.ts.map +1 -0
- package/dist/beta/index.js +7 -0
- package/dist/beta/index.js.map +1 -0
- package/dist/beta/workflows/index.cjs +29 -0
- package/dist/beta/workflows/index.cjs.map +1 -0
- package/dist/beta/workflows/index.d.cts +2 -0
- package/dist/beta/workflows/index.d.ts +2 -0
- package/dist/beta/workflows/index.d.ts.map +1 -0
- package/dist/beta/workflows/index.js +7 -0
- package/dist/beta/workflows/index.js.map +1 -0
- package/dist/beta/workflows/task_group.cjs +162 -0
- package/dist/beta/workflows/task_group.cjs.map +1 -0
- package/dist/beta/workflows/task_group.d.cts +32 -0
- package/dist/beta/workflows/task_group.d.ts +32 -0
- package/dist/beta/workflows/task_group.d.ts.map +1 -0
- package/dist/beta/workflows/task_group.js +138 -0
- package/dist/beta/workflows/task_group.js.map +1 -0
- package/dist/cli.cjs +14 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -20
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/inference/api_protos.d.cts +59 -59
- package/dist/inference/api_protos.d.ts +59 -59
- package/dist/ipc/job_proc_lazy_main.cjs +14 -5
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +14 -5
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/llm/chat_context.cjs +108 -1
- package/dist/llm/chat_context.cjs.map +1 -1
- package/dist/llm/chat_context.d.cts +14 -1
- package/dist/llm/chat_context.d.ts +14 -1
- package/dist/llm/chat_context.d.ts.map +1 -1
- package/dist/llm/chat_context.js +108 -1
- package/dist/llm/chat_context.js.map +1 -1
- package/dist/llm/chat_context.test.cjs +43 -0
- package/dist/llm/chat_context.test.cjs.map +1 -1
- package/dist/llm/chat_context.test.js +43 -0
- package/dist/llm/chat_context.test.js.map +1 -1
- package/dist/llm/index.cjs +2 -0
- package/dist/llm/index.cjs.map +1 -1
- package/dist/llm/index.d.cts +1 -1
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +3 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/provider_format/index.cjs +2 -0
- package/dist/llm/provider_format/index.cjs.map +1 -1
- package/dist/llm/provider_format/index.d.cts +2 -2
- package/dist/llm/provider_format/index.d.ts +2 -2
- package/dist/llm/provider_format/index.d.ts.map +1 -1
- package/dist/llm/provider_format/index.js +6 -1
- package/dist/llm/provider_format/index.js.map +1 -1
- package/dist/llm/provider_format/openai.cjs +82 -2
- package/dist/llm/provider_format/openai.cjs.map +1 -1
- package/dist/llm/provider_format/openai.d.cts +1 -0
- package/dist/llm/provider_format/openai.d.ts +1 -0
- package/dist/llm/provider_format/openai.d.ts.map +1 -1
- package/dist/llm/provider_format/openai.js +80 -1
- package/dist/llm/provider_format/openai.js.map +1 -1
- package/dist/llm/provider_format/openai.test.cjs +326 -0
- package/dist/llm/provider_format/openai.test.cjs.map +1 -1
- package/dist/llm/provider_format/openai.test.js +327 -1
- package/dist/llm/provider_format/openai.test.js.map +1 -1
- package/dist/llm/provider_format/utils.cjs +4 -3
- package/dist/llm/provider_format/utils.cjs.map +1 -1
- package/dist/llm/provider_format/utils.d.ts.map +1 -1
- package/dist/llm/provider_format/utils.js +4 -3
- package/dist/llm/provider_format/utils.js.map +1 -1
- package/dist/llm/realtime.cjs.map +1 -1
- package/dist/llm/realtime.d.cts +1 -0
- package/dist/llm/realtime.d.ts +1 -0
- package/dist/llm/realtime.d.ts.map +1 -1
- package/dist/llm/realtime.js.map +1 -1
- package/dist/llm/tool_context.cjs +7 -0
- package/dist/llm/tool_context.cjs.map +1 -1
- package/dist/llm/tool_context.d.cts +10 -2
- package/dist/llm/tool_context.d.ts +10 -2
- package/dist/llm/tool_context.d.ts.map +1 -1
- package/dist/llm/tool_context.js +6 -0
- package/dist/llm/tool_context.js.map +1 -1
- package/dist/log.cjs +5 -2
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +5 -2
- package/dist/log.js.map +1 -1
- package/dist/stream/deferred_stream.cjs +15 -6
- package/dist/stream/deferred_stream.cjs.map +1 -1
- package/dist/stream/deferred_stream.d.ts.map +1 -1
- package/dist/stream/deferred_stream.js +15 -6
- package/dist/stream/deferred_stream.js.map +1 -1
- package/dist/utils.cjs +32 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +7 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +32 -2
- package/dist/utils.js.map +1 -1
- package/dist/utils.test.cjs +71 -0
- package/dist/utils.test.cjs.map +1 -1
- package/dist/utils.test.js +71 -0
- package/dist/utils.test.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/voice/agent.cjs +153 -12
- package/dist/voice/agent.cjs.map +1 -1
- package/dist/voice/agent.d.cts +30 -4
- package/dist/voice/agent.d.ts +30 -4
- package/dist/voice/agent.d.ts.map +1 -1
- package/dist/voice/agent.js +149 -11
- package/dist/voice/agent.js.map +1 -1
- package/dist/voice/agent.test.cjs +120 -0
- package/dist/voice/agent.test.cjs.map +1 -1
- package/dist/voice/agent.test.js +122 -2
- package/dist/voice/agent.test.js.map +1 -1
- package/dist/voice/agent_activity.cjs +406 -298
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +41 -7
- package/dist/voice/agent_activity.d.ts +41 -7
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +407 -294
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +140 -40
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +19 -7
- package/dist/voice/agent_session.d.ts +19 -7
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +137 -37
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/audio_recognition.cjs +4 -0
- package/dist/voice/audio_recognition.cjs.map +1 -1
- package/dist/voice/audio_recognition.d.ts.map +1 -1
- package/dist/voice/audio_recognition.js +4 -0
- package/dist/voice/audio_recognition.js.map +1 -1
- package/dist/voice/generation.cjs +39 -19
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +44 -20
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/index.cjs +2 -0
- package/dist/voice/index.cjs.map +1 -1
- package/dist/voice/index.d.cts +1 -1
- package/dist/voice/index.d.ts +1 -1
- package/dist/voice/index.d.ts.map +1 -1
- package/dist/voice/index.js +2 -1
- package/dist/voice/index.js.map +1 -1
- package/dist/voice/room_io/room_io.cjs +11 -2
- package/dist/voice/room_io/room_io.cjs.map +1 -1
- package/dist/voice/room_io/room_io.d.ts.map +1 -1
- package/dist/voice/room_io/room_io.js +12 -3
- package/dist/voice/room_io/room_io.js.map +1 -1
- package/dist/voice/speech_handle.cjs +7 -1
- package/dist/voice/speech_handle.cjs.map +1 -1
- package/dist/voice/speech_handle.d.cts +2 -0
- package/dist/voice/speech_handle.d.ts +2 -0
- package/dist/voice/speech_handle.d.ts.map +1 -1
- package/dist/voice/speech_handle.js +8 -2
- package/dist/voice/speech_handle.js.map +1 -1
- package/dist/voice/testing/fake_llm.cjs +127 -0
- package/dist/voice/testing/fake_llm.cjs.map +1 -0
- package/dist/voice/testing/fake_llm.d.cts +30 -0
- package/dist/voice/testing/fake_llm.d.ts +30 -0
- package/dist/voice/testing/fake_llm.d.ts.map +1 -0
- package/dist/voice/testing/fake_llm.js +103 -0
- package/dist/voice/testing/fake_llm.js.map +1 -0
- package/dist/voice/testing/index.cjs +3 -0
- package/dist/voice/testing/index.cjs.map +1 -1
- package/dist/voice/testing/index.d.cts +1 -0
- package/dist/voice/testing/index.d.ts +1 -0
- package/dist/voice/testing/index.d.ts.map +1 -1
- package/dist/voice/testing/index.js +2 -0
- package/dist/voice/testing/index.js.map +1 -1
- package/dist/voice/testing/run_result.cjs +66 -15
- package/dist/voice/testing/run_result.cjs.map +1 -1
- package/dist/voice/testing/run_result.d.cts +14 -3
- package/dist/voice/testing/run_result.d.ts +14 -3
- package/dist/voice/testing/run_result.d.ts.map +1 -1
- package/dist/voice/testing/run_result.js +66 -15
- package/dist/voice/testing/run_result.js.map +1 -1
- package/package.json +1 -1
- package/src/beta/index.ts +9 -0
- package/src/beta/workflows/index.ts +9 -0
- package/src/beta/workflows/task_group.ts +194 -0
- package/src/cli.ts +20 -33
- package/src/index.ts +2 -1
- package/src/ipc/job_proc_lazy_main.ts +16 -5
- package/src/llm/chat_context.test.ts +48 -0
- package/src/llm/chat_context.ts +158 -0
- package/src/llm/index.ts +1 -0
- package/src/llm/provider_format/index.ts +7 -2
- package/src/llm/provider_format/openai.test.ts +385 -1
- package/src/llm/provider_format/openai.ts +103 -0
- package/src/llm/provider_format/utils.ts +6 -4
- package/src/llm/realtime.ts +1 -0
- package/src/llm/tool_context.ts +14 -0
- package/src/log.ts +5 -2
- package/src/stream/deferred_stream.ts +17 -6
- package/src/utils.test.ts +87 -0
- package/src/utils.ts +41 -2
- package/src/version.ts +1 -1
- package/src/voice/agent.test.ts +140 -2
- package/src/voice/agent.ts +200 -10
- package/src/voice/agent_activity.ts +466 -290
- package/src/voice/agent_session.ts +178 -40
- package/src/voice/audio_recognition.ts +4 -0
- package/src/voice/generation.ts +52 -23
- package/src/voice/index.ts +1 -1
- package/src/voice/room_io/room_io.ts +14 -3
- package/src/voice/speech_handle.ts +9 -2
- package/src/voice/testing/fake_llm.ts +138 -0
- package/src/voice/testing/index.ts +2 -0
- package/src/voice/testing/run_result.ts +81 -23
package/src/utils.test.ts
CHANGED
|
@@ -469,6 +469,93 @@ describe('utils', () => {
|
|
|
469
469
|
expect((error as Error).name).toBe('TypeError');
|
|
470
470
|
}
|
|
471
471
|
});
|
|
472
|
+
|
|
473
|
+
it('should return undefined for Task.current outside task context', () => {
|
|
474
|
+
expect(Task.current()).toBeUndefined();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should preserve Task.current inside a task across awaits', async () => {
|
|
478
|
+
const task = Task.from(
|
|
479
|
+
async () => {
|
|
480
|
+
const currentAtStart = Task.current();
|
|
481
|
+
await delay(5);
|
|
482
|
+
const currentAfterAwait = Task.current();
|
|
483
|
+
|
|
484
|
+
expect(currentAtStart).toBeDefined();
|
|
485
|
+
expect(currentAfterAwait).toBe(currentAtStart);
|
|
486
|
+
|
|
487
|
+
return currentAtStart;
|
|
488
|
+
},
|
|
489
|
+
undefined,
|
|
490
|
+
'current-context-test',
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const currentFromResult = await task.result;
|
|
494
|
+
expect(currentFromResult).toBe(task);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should isolate nested Task.current context and restore parent context', async () => {
|
|
498
|
+
const parentTask = Task.from(
|
|
499
|
+
async (controller) => {
|
|
500
|
+
const parentCurrent = Task.current();
|
|
501
|
+
expect(parentCurrent).toBeDefined();
|
|
502
|
+
|
|
503
|
+
const childTask = Task.from(
|
|
504
|
+
async () => {
|
|
505
|
+
const childCurrentStart = Task.current();
|
|
506
|
+
await delay(5);
|
|
507
|
+
const childCurrentAfterAwait = Task.current();
|
|
508
|
+
|
|
509
|
+
expect(childCurrentStart).toBeDefined();
|
|
510
|
+
expect(childCurrentAfterAwait).toBe(childCurrentStart);
|
|
511
|
+
expect(childCurrentStart).not.toBe(parentCurrent);
|
|
512
|
+
|
|
513
|
+
return childCurrentStart;
|
|
514
|
+
},
|
|
515
|
+
controller,
|
|
516
|
+
'child-current-context-test',
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const childCurrent = await childTask.result;
|
|
520
|
+
const parentCurrentAfterChild = Task.current();
|
|
521
|
+
|
|
522
|
+
expect(parentCurrentAfterChild).toBe(parentCurrent);
|
|
523
|
+
|
|
524
|
+
return { parentCurrent, childCurrent };
|
|
525
|
+
},
|
|
526
|
+
undefined,
|
|
527
|
+
'parent-current-context-test',
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const { parentCurrent, childCurrent } = await parentTask.result;
|
|
531
|
+
expect(parentCurrent).toBe(parentTask);
|
|
532
|
+
expect(childCurrent).not.toBe(parentCurrent);
|
|
533
|
+
expect(Task.current()).toBeUndefined();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should always expose Task.current for concurrent task callbacks', async () => {
|
|
537
|
+
const tasks = Array.from({ length: 25 }, (_, idx) =>
|
|
538
|
+
Task.from(
|
|
539
|
+
async () => {
|
|
540
|
+
const currentAtStart = Task.current();
|
|
541
|
+
await delay(1);
|
|
542
|
+
const currentAfterAwait = Task.current();
|
|
543
|
+
|
|
544
|
+
expect(currentAtStart).toBeDefined();
|
|
545
|
+
expect(currentAfterAwait).toBe(currentAtStart);
|
|
546
|
+
|
|
547
|
+
return currentAtStart;
|
|
548
|
+
},
|
|
549
|
+
undefined,
|
|
550
|
+
`current-context-stress-${idx}`,
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const currentTasks = await Promise.all(tasks.map((task) => task.result));
|
|
555
|
+
currentTasks.forEach((currentTask, idx) => {
|
|
556
|
+
expect(currentTask).toBe(tasks[idx]);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
472
559
|
});
|
|
473
560
|
|
|
474
561
|
describe('Event', () => {
|
package/src/utils.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TrackKind,
|
|
10
10
|
} from '@livekit/rtc-node';
|
|
11
11
|
import { AudioFrame, AudioResampler, RoomEvent } from '@livekit/rtc-node';
|
|
12
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
12
13
|
import { EventEmitter, once } from 'node:events';
|
|
13
14
|
import type { ReadableStream } from 'node:stream/web';
|
|
14
15
|
import { TransformStream, type TransformStreamDefaultController } from 'node:stream/web';
|
|
@@ -172,6 +173,11 @@ export class Future<T = void> {
|
|
|
172
173
|
this.#rejected = true;
|
|
173
174
|
this.#error = error;
|
|
174
175
|
this.#rejectPromise(error);
|
|
176
|
+
// Python calls Future.exception() right after set_exception() to silence
|
|
177
|
+
// "exception was never retrieved" warnings. In JS, consume the rejection
|
|
178
|
+
// immediately so Node does not emit unhandled-rejection noise before a
|
|
179
|
+
// later await/catch observes it.
|
|
180
|
+
void this.#await.catch(() => undefined);
|
|
175
181
|
}
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -434,7 +440,9 @@ export enum TaskResult {
|
|
|
434
440
|
* @param T - The type of the task result
|
|
435
441
|
*/
|
|
436
442
|
export class Task<T> {
|
|
443
|
+
private static readonly currentTaskStorage = new AsyncLocalStorage<Task<unknown>>();
|
|
437
444
|
private resultFuture: Future<T>;
|
|
445
|
+
private doneCallbacks: Set<() => void> = new Set();
|
|
438
446
|
|
|
439
447
|
#logger = log();
|
|
440
448
|
|
|
@@ -444,6 +452,21 @@ export class Task<T> {
|
|
|
444
452
|
readonly name?: string,
|
|
445
453
|
) {
|
|
446
454
|
this.resultFuture = new Future();
|
|
455
|
+
void this.resultFuture.await
|
|
456
|
+
.then(
|
|
457
|
+
() => undefined,
|
|
458
|
+
() => undefined,
|
|
459
|
+
)
|
|
460
|
+
.finally(() => {
|
|
461
|
+
for (const callback of this.doneCallbacks) {
|
|
462
|
+
try {
|
|
463
|
+
callback();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
this.#logger.error({ error }, 'Task done callback failed');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
this.doneCallbacks.clear();
|
|
469
|
+
});
|
|
447
470
|
this.runTask();
|
|
448
471
|
}
|
|
449
472
|
|
|
@@ -463,6 +486,13 @@ export class Task<T> {
|
|
|
463
486
|
return new Task(fn, abortController, name);
|
|
464
487
|
}
|
|
465
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Returns the currently running task in this async context, if available.
|
|
491
|
+
*/
|
|
492
|
+
static current(): Task<unknown> | undefined {
|
|
493
|
+
return Task.currentTaskStorage.getStore();
|
|
494
|
+
}
|
|
495
|
+
|
|
466
496
|
private async runTask() {
|
|
467
497
|
const run = async () => {
|
|
468
498
|
if (this.name) {
|
|
@@ -471,7 +501,8 @@ export class Task<T> {
|
|
|
471
501
|
return await this.fn(this.controller);
|
|
472
502
|
};
|
|
473
503
|
|
|
474
|
-
return
|
|
504
|
+
return Task.currentTaskStorage
|
|
505
|
+
.run(this as Task<unknown>, run)
|
|
475
506
|
.then((value) => {
|
|
476
507
|
this.resultFuture.resolve(value);
|
|
477
508
|
return value;
|
|
@@ -543,7 +574,15 @@ export class Task<T> {
|
|
|
543
574
|
}
|
|
544
575
|
|
|
545
576
|
addDoneCallback(callback: () => void) {
|
|
546
|
-
this.
|
|
577
|
+
if (this.done) {
|
|
578
|
+
queueMicrotask(callback);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.doneCallbacks.add(callback);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
removeDoneCallback(callback: () => void) {
|
|
585
|
+
this.doneCallbacks.delete(callback);
|
|
547
586
|
}
|
|
548
587
|
}
|
|
549
588
|
|
package/src/version.ts
CHANGED
package/src/voice/agent.test.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { tool } from '../llm/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { initializeLogger } from '../log.js';
|
|
8
|
+
import { Task } from '../utils.js';
|
|
9
|
+
import { Agent, AgentTask, _setActivityTaskInfo } from './agent.js';
|
|
10
|
+
import { agentActivityStorage } from './agent_activity.js';
|
|
11
|
+
|
|
12
|
+
initializeLogger({ pretty: false, level: 'error' });
|
|
8
13
|
|
|
9
14
|
describe('Agent', () => {
|
|
10
15
|
it('should create agent with basic instructions', () => {
|
|
@@ -77,4 +82,137 @@ describe('Agent', () => {
|
|
|
77
82
|
expect(tools1).toEqual(tools2);
|
|
78
83
|
expect(tools1).toEqual(tools);
|
|
79
84
|
});
|
|
85
|
+
|
|
86
|
+
it('should require AgentTask to run inside task context', async () => {
|
|
87
|
+
class TestTask extends AgentTask<string> {
|
|
88
|
+
constructor() {
|
|
89
|
+
super({ instructions: 'test task' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const task = new TestTask();
|
|
94
|
+
await expect(task.run()).rejects.toThrow('must be executed inside a Task context');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should require AgentTask to run inside inline task context', async () => {
|
|
98
|
+
class TestTask extends AgentTask<string> {
|
|
99
|
+
constructor() {
|
|
100
|
+
super({ instructions: 'test task' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const task = new TestTask();
|
|
105
|
+
const wrapper = Task.from(async () => {
|
|
106
|
+
return await task.run();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await expect(wrapper.result).rejects.toThrow(
|
|
110
|
+
'should only be awaited inside function tools or the onEnter/onExit methods of an Agent',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should allow AgentTask run from inline task context', async () => {
|
|
115
|
+
class TestTask extends AgentTask<string> {
|
|
116
|
+
constructor() {
|
|
117
|
+
super({ instructions: 'test task' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const task = new TestTask();
|
|
122
|
+
const oldAgent = new Agent({ instructions: 'old agent' });
|
|
123
|
+
const mockSession = {
|
|
124
|
+
currentAgent: oldAgent,
|
|
125
|
+
_globalRunState: undefined,
|
|
126
|
+
_updateActivity: async (agent: Agent) => {
|
|
127
|
+
if (agent === task) {
|
|
128
|
+
task.complete('ok');
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const mockActivity = {
|
|
134
|
+
agent: oldAgent,
|
|
135
|
+
agentSession: mockSession,
|
|
136
|
+
_onEnterTask: undefined,
|
|
137
|
+
llm: undefined,
|
|
138
|
+
close: async () => {},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const wrapper = Task.from(async () => {
|
|
142
|
+
const currentTask = Task.current();
|
|
143
|
+
if (!currentTask) {
|
|
144
|
+
throw new Error('expected task context');
|
|
145
|
+
}
|
|
146
|
+
_setActivityTaskInfo(currentTask, { inlineTask: true });
|
|
147
|
+
return await agentActivityStorage.run(mockActivity as any, () => task.run());
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await expect(wrapper.result).resolves.toBe('ok');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should require AgentTask to run inside AgentActivity context', async () => {
|
|
154
|
+
class TestTask extends AgentTask<string> {
|
|
155
|
+
constructor() {
|
|
156
|
+
super({ instructions: 'test task' });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const task = new TestTask();
|
|
161
|
+
const wrapper = Task.from(async () => {
|
|
162
|
+
const currentTask = Task.current();
|
|
163
|
+
if (!currentTask) {
|
|
164
|
+
throw new Error('expected task context');
|
|
165
|
+
}
|
|
166
|
+
_setActivityTaskInfo(currentTask, { inlineTask: true });
|
|
167
|
+
return await task.run();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await expect(wrapper.result).rejects.toThrow(
|
|
171
|
+
'must be executed inside an AgentActivity context',
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should close old activity when current agent changes while AgentTask is pending', async () => {
|
|
176
|
+
class TestTask extends AgentTask<string> {
|
|
177
|
+
constructor() {
|
|
178
|
+
super({ instructions: 'test task' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const task = new TestTask();
|
|
183
|
+
const oldAgent = new Agent({ instructions: 'old agent' });
|
|
184
|
+
const switchedAgent = new Agent({ instructions: 'switched agent' });
|
|
185
|
+
const closeOldActivity = vi.fn(async () => {});
|
|
186
|
+
|
|
187
|
+
const mockSession = {
|
|
188
|
+
currentAgent: oldAgent as Agent,
|
|
189
|
+
_globalRunState: undefined,
|
|
190
|
+
_updateActivity: async (agent: Agent) => {
|
|
191
|
+
if (agent === task) {
|
|
192
|
+
mockSession.currentAgent = switchedAgent;
|
|
193
|
+
task.complete('ok');
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const mockActivity = {
|
|
199
|
+
agent: oldAgent,
|
|
200
|
+
agentSession: mockSession,
|
|
201
|
+
_onEnterTask: undefined,
|
|
202
|
+
llm: undefined,
|
|
203
|
+
close: closeOldActivity,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const wrapper = Task.from(async () => {
|
|
207
|
+
const currentTask = Task.current();
|
|
208
|
+
if (!currentTask) {
|
|
209
|
+
throw new Error('expected task context');
|
|
210
|
+
}
|
|
211
|
+
_setActivityTaskInfo(currentTask, { inlineTask: true });
|
|
212
|
+
return await agentActivityStorage.run(mockActivity as any, () => task.run());
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await expect(wrapper.result).resolves.toBe('ok');
|
|
216
|
+
expect(closeOldActivity).toHaveBeenCalledTimes(1);
|
|
217
|
+
});
|
|
80
218
|
});
|
package/src/voice/agent.ts
CHANGED
|
@@ -13,26 +13,71 @@ import {
|
|
|
13
13
|
type TTSModelString,
|
|
14
14
|
} from '../inference/index.js';
|
|
15
15
|
import { ReadonlyChatContext } from '../llm/chat_context.js';
|
|
16
|
-
import type { ChatMessage, FunctionCall
|
|
16
|
+
import type { ChatMessage, FunctionCall } from '../llm/index.js';
|
|
17
17
|
import {
|
|
18
18
|
type ChatChunk,
|
|
19
19
|
ChatContext,
|
|
20
20
|
LLM,
|
|
21
|
+
RealtimeModel,
|
|
21
22
|
type ToolChoice,
|
|
22
23
|
type ToolContext,
|
|
23
24
|
} from '../llm/index.js';
|
|
25
|
+
import { log } from '../log.js';
|
|
24
26
|
import type { STT, SpeechEvent } from '../stt/index.js';
|
|
25
27
|
import { StreamAdapter as STTStreamAdapter } from '../stt/index.js';
|
|
26
28
|
import { SentenceTokenizer as BasicSentenceTokenizer } from '../tokenize/basic/index.js';
|
|
27
29
|
import type { TTS } from '../tts/index.js';
|
|
28
30
|
import { SynthesizeStream, StreamAdapter as TTSStreamAdapter } from '../tts/index.js';
|
|
29
31
|
import { USERDATA_TIMED_TRANSCRIPT } from '../types.js';
|
|
32
|
+
import { Future, Task } from '../utils.js';
|
|
30
33
|
import type { VAD } from '../vad.js';
|
|
31
|
-
import type
|
|
34
|
+
import { type AgentActivity, agentActivityStorage } from './agent_activity.js';
|
|
32
35
|
import type { AgentSession, TurnDetectionMode } from './agent_session.js';
|
|
33
36
|
import type { TimedString } from './io.js';
|
|
37
|
+
import type { SpeechHandle } from './speech_handle.js';
|
|
38
|
+
|
|
39
|
+
export const functionCallStorage = new AsyncLocalStorage<{ functionCall?: FunctionCall }>();
|
|
40
|
+
export const speechHandleStorage = new AsyncLocalStorage<SpeechHandle>();
|
|
41
|
+
const activityTaskInfoStorage = new WeakMap<Task<any>, _ActivityTaskInfo>();
|
|
42
|
+
|
|
43
|
+
type _ActivityTaskInfo = {
|
|
44
|
+
functionCall: FunctionCall | null;
|
|
45
|
+
speechHandle: SpeechHandle | null;
|
|
46
|
+
inlineTask: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export function _setActivityTaskInfo<T>(
|
|
51
|
+
task: Task<T>,
|
|
52
|
+
options: {
|
|
53
|
+
functionCall?: FunctionCall | null;
|
|
54
|
+
speechHandle?: SpeechHandle | null;
|
|
55
|
+
inlineTask?: boolean;
|
|
56
|
+
},
|
|
57
|
+
): void {
|
|
58
|
+
const info = activityTaskInfoStorage.get(task) ?? {
|
|
59
|
+
functionCall: null,
|
|
60
|
+
speechHandle: null,
|
|
61
|
+
inlineTask: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (Object.hasOwn(options, 'functionCall')) {
|
|
65
|
+
info.functionCall = options.functionCall ?? null;
|
|
66
|
+
}
|
|
67
|
+
if (Object.hasOwn(options, 'speechHandle')) {
|
|
68
|
+
info.speechHandle = options.speechHandle ?? null;
|
|
69
|
+
}
|
|
70
|
+
if (Object.hasOwn(options, 'inlineTask')) {
|
|
71
|
+
info.inlineTask = options.inlineTask ?? false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
activityTaskInfoStorage.set(task, info);
|
|
75
|
+
}
|
|
34
76
|
|
|
35
|
-
|
|
77
|
+
/** @internal */
|
|
78
|
+
export function _getActivityTaskInfo<T>(task: Task<T>): _ActivityTaskInfo | undefined {
|
|
79
|
+
return activityTaskInfoStorage.get(task);
|
|
80
|
+
}
|
|
36
81
|
export const STOP_RESPONSE_SYMBOL = Symbol('StopResponse');
|
|
37
82
|
|
|
38
83
|
export class StopResponse extends Error {
|
|
@@ -257,6 +302,17 @@ export class Agent<UserData = any> {
|
|
|
257
302
|
this._agentActivity.updateChatCtx(chatCtx);
|
|
258
303
|
}
|
|
259
304
|
|
|
305
|
+
// TODO(parity): Add when AgentConfigUpdate is ported to ChatContext.
|
|
306
|
+
async updateTools(tools: ToolContext): Promise<void> {
|
|
307
|
+
if (!this._agentActivity) {
|
|
308
|
+
this._tools = { ...tools };
|
|
309
|
+
this._chatCtx = this._chatCtx.copy({ toolCtx: this._tools });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await this._agentActivity.updateTools(tools);
|
|
314
|
+
}
|
|
315
|
+
|
|
260
316
|
static default = {
|
|
261
317
|
async sttNode(
|
|
262
318
|
agent: Agent,
|
|
@@ -268,20 +324,20 @@ export class Agent<UserData = any> {
|
|
|
268
324
|
throw new Error('sttNode called but no STT node is available');
|
|
269
325
|
}
|
|
270
326
|
|
|
271
|
-
let
|
|
327
|
+
let wrappedStt = activity.stt;
|
|
272
328
|
|
|
273
|
-
if (!
|
|
329
|
+
if (!wrappedStt.capabilities.streaming) {
|
|
274
330
|
const vad = agent.vad || activity.vad;
|
|
275
331
|
if (!vad) {
|
|
276
332
|
throw new Error(
|
|
277
333
|
'STT does not support streaming, add a VAD to the AgentTask/VoiceAgent to enable streaming',
|
|
278
334
|
);
|
|
279
335
|
}
|
|
280
|
-
|
|
336
|
+
wrappedStt = new STTStreamAdapter(wrappedStt, vad);
|
|
281
337
|
}
|
|
282
338
|
|
|
283
339
|
const connOptions = activity.agentSession.connOptions.sttConnOptions;
|
|
284
|
-
const stream =
|
|
340
|
+
const stream = wrappedStt.stream({ connOptions });
|
|
285
341
|
|
|
286
342
|
// Set startTimeOffset to provide linear timestamps across reconnections
|
|
287
343
|
const audioInputStartedAt =
|
|
@@ -382,14 +438,14 @@ export class Agent<UserData = any> {
|
|
|
382
438
|
throw new Error('ttsNode called but no TTS node is available');
|
|
383
439
|
}
|
|
384
440
|
|
|
385
|
-
let
|
|
441
|
+
let wrappedTts = activity.tts;
|
|
386
442
|
|
|
387
443
|
if (!activity.tts.capabilities.streaming) {
|
|
388
|
-
|
|
444
|
+
wrappedTts = new TTSStreamAdapter(wrappedTts, new BasicSentenceTokenizer());
|
|
389
445
|
}
|
|
390
446
|
|
|
391
447
|
const connOptions = activity.agentSession.connOptions.ttsConnOptions;
|
|
392
|
-
const stream =
|
|
448
|
+
const stream = wrappedTts.stream({ connOptions });
|
|
393
449
|
stream.updateInputStream(text);
|
|
394
450
|
|
|
395
451
|
let cleaned = false;
|
|
@@ -440,3 +496,137 @@ export class Agent<UserData = any> {
|
|
|
440
496
|
},
|
|
441
497
|
};
|
|
442
498
|
}
|
|
499
|
+
|
|
500
|
+
export class AgentTask<ResultT = unknown, UserData = any> extends Agent<UserData> {
|
|
501
|
+
private started = false;
|
|
502
|
+
private future = new Future<ResultT>();
|
|
503
|
+
|
|
504
|
+
#logger = log();
|
|
505
|
+
|
|
506
|
+
get done(): boolean {
|
|
507
|
+
return this.future.done;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
complete(result: ResultT | Error): void {
|
|
511
|
+
if (this.future.done) {
|
|
512
|
+
throw new Error(`${this.constructor.name} is already done`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (result instanceof Error) {
|
|
516
|
+
this.future.reject(result);
|
|
517
|
+
} else {
|
|
518
|
+
this.future.resolve(result);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const speechHandle = speechHandleStorage.getStore();
|
|
522
|
+
if (speechHandle) {
|
|
523
|
+
speechHandle._maybeRunFinalOutput = result;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async run(): Promise<ResultT> {
|
|
528
|
+
if (this.started) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Task ${this.constructor.name} has already started and cannot be awaited multiple times`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
this.started = true;
|
|
534
|
+
|
|
535
|
+
const currentTask = Task.current();
|
|
536
|
+
if (!currentTask) {
|
|
537
|
+
throw new Error(`${this.constructor.name} must be executed inside a Task context`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const taskInfo = _getActivityTaskInfo(currentTask);
|
|
541
|
+
if (!taskInfo || !taskInfo.inlineTask) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`${this.constructor.name} should only be awaited inside function tools or the onEnter/onExit methods of an Agent`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const speechHandle = speechHandleStorage.getStore();
|
|
548
|
+
const oldActivity = agentActivityStorage.getStore();
|
|
549
|
+
if (!oldActivity) {
|
|
550
|
+
throw new Error(`${this.constructor.name} must be executed inside an AgentActivity context`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
currentTask.addDoneCallback(() => {
|
|
554
|
+
if (this.future.done) return;
|
|
555
|
+
|
|
556
|
+
// If the Task finished before the AgentTask was completed, complete the AgentTask with an error.
|
|
557
|
+
this.#logger.error(`The Task finished before ${this.constructor.name} was completed.`);
|
|
558
|
+
this.complete(new Error(`The Task finished before ${this.constructor.name} was completed.`));
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const oldAgent = oldActivity.agent;
|
|
562
|
+
const session = oldActivity.agentSession;
|
|
563
|
+
|
|
564
|
+
const blockedTasks: Task<any>[] = [currentTask];
|
|
565
|
+
const onEnterTask = oldActivity._onEnterTask;
|
|
566
|
+
|
|
567
|
+
if (onEnterTask && !onEnterTask.done && onEnterTask !== currentTask) {
|
|
568
|
+
blockedTasks.push(onEnterTask);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (
|
|
572
|
+
taskInfo.functionCall &&
|
|
573
|
+
oldActivity.llm instanceof RealtimeModel &&
|
|
574
|
+
!oldActivity.llm.capabilities.manualFunctionCalls
|
|
575
|
+
) {
|
|
576
|
+
this.#logger.error(
|
|
577
|
+
`Realtime model does not support resuming function calls from chat context, ` +
|
|
578
|
+
`using AgentTask inside a function tool may have unexpected behavior.`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
await session._updateActivity(this, {
|
|
583
|
+
previousActivity: 'pause',
|
|
584
|
+
newActivity: 'start',
|
|
585
|
+
blockedTasks,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
let runState = session._globalRunState;
|
|
589
|
+
if (speechHandle && runState && !runState.done()) {
|
|
590
|
+
// Only unwatch the parent speech handle if there are other handles keeping the run alive.
|
|
591
|
+
// When watchedHandleCount is 1 (only the parent), unwatching would drop it to 0 and
|
|
592
|
+
// mark the run done prematurely — before function_call_output and assistant message arrive.
|
|
593
|
+
if (runState._watchedHandleCount() > 1) {
|
|
594
|
+
runState._unwatchHandle(speechHandle);
|
|
595
|
+
}
|
|
596
|
+
// it is OK to call _markDoneIfNeeded here, the above _updateActivity will call onEnter
|
|
597
|
+
// and newly added handles keep the run alive.
|
|
598
|
+
runState._markDoneIfNeeded();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
return await this.future.await;
|
|
603
|
+
} finally {
|
|
604
|
+
// runState could have changed after future resolved
|
|
605
|
+
runState = session._globalRunState;
|
|
606
|
+
|
|
607
|
+
if (session.currentAgent !== this) {
|
|
608
|
+
this.#logger.warn(
|
|
609
|
+
`${this.constructor.name} completed, but the agent has changed in the meantime. ` +
|
|
610
|
+
`Ignoring handoff to the previous agent, likely due to AgentSession.updateAgent being invoked.`,
|
|
611
|
+
);
|
|
612
|
+
await oldActivity.close();
|
|
613
|
+
} else {
|
|
614
|
+
if (speechHandle && runState && !runState.done()) {
|
|
615
|
+
runState._watchHandle(speechHandle);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const mergedChatCtx = oldAgent._chatCtx.merge(this._chatCtx, {
|
|
619
|
+
excludeFunctionCall: true,
|
|
620
|
+
excludeInstructions: true,
|
|
621
|
+
});
|
|
622
|
+
oldAgent._chatCtx.items = mergedChatCtx.items;
|
|
623
|
+
|
|
624
|
+
await session._updateActivity(oldAgent, {
|
|
625
|
+
previousActivity: 'close',
|
|
626
|
+
newActivity: 'resume',
|
|
627
|
+
waitOnEnter: false,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|