Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/await-realtime-chatctx-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents': patch
---

Await active realtime chat context updates through `Agent.updateChatCtx()` so callers can reliably sequence follow-up model turns after conversation item sync completes.
Comment on lines +1 to +5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 New changeset file is missing the required SPDX license header

The newly added .changeset/await-realtime-chatctx-sync.md file starts directly with frontmatter and does not include the repository-required SPDX header block, which violates the mandatory contribution rules for new files. This creates a compliance regression in the PR and should be fixed before merge (.changeset/await-realtime-chatctx-sync.md:1-5).

Suggested change
---
'@livekit/agents': patch
---
Await active realtime chat context updates through `Agent.updateChatCtx()` so callers can reliably sequence follow-up model turns after conversation item sync completes.
<!--
SPDX-FileCopyrightText: 2026 LiveKit, Inc.
SPDX-License-Identifier: Apache-2.0
-->
---
'@livekit/agents': patch
---
Await active realtime chat context updates through `Agent.updateChatCtx()` so callers can reliably sequence follow-up model turns after conversation item sync completes.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for checking. I think this file should stay as-is because .changeset/** is covered by the repo-level REUSE annotations rather than inline SPDX headers. REUSE.toml explicitly annotates .changeset/** under “trivial files” with LiveKit copyright and Apache-2.0, and existing changesets on main such as .changeset/assemblyai-inference-model.md also omit inline SPDX headers. So this appears consistent with the repo convention.

95 changes: 95 additions & 0 deletions agents/src/voice/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: Apache-2.0
import { describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { ChatContext } from '../llm/chat_context.js';
import { tool } from '../llm/index.js';
import { initializeLogger } from '../log.js';
import { Task } from '../utils.js';
Expand All @@ -24,6 +25,49 @@ describe('Agent', () => {
expect(agent.instructions).toBe(instructions);
});

it('should wait for active activity chat context updates', async () => {
const agent = new Agent({ instructions: 'test' });
const chatCtx = new ChatContext();
let resolveUpdate: () => void = () => {
throw new Error('update promise was not initialized');
};
const updatePromise = new Promise<void>((resolve) => {
resolveUpdate = resolve;
});
const updateChatCtx = vi.fn(() => updatePromise);
(
agent as unknown as { _agentActivity: { updateChatCtx: typeof updateChatCtx } }
)._agentActivity = { updateChatCtx };

let settled = false;
const update = agent.updateChatCtx(chatCtx).then(() => {
settled = true;
});

await Promise.resolve();

expect(updateChatCtx).toHaveBeenCalledWith(chatCtx);
expect(settled).toBe(false);

resolveUpdate();
await update;

expect(settled).toBe(true);
});

it('should propagate active activity chat context update failures', async () => {
const agent = new Agent({ instructions: 'test' });
const chatCtx = new ChatContext();
const error = new Error('update failed');
const updateChatCtx = vi.fn(() => Promise.reject(error));
(
agent as unknown as { _agentActivity: { updateChatCtx: typeof updateChatCtx } }
)._agentActivity = { updateChatCtx };

await expect(agent.updateChatCtx(chatCtx)).rejects.toBe(error);
expect(updateChatCtx).toHaveBeenCalledWith(chatCtx);
});

it('should create agent with instructions and tools', () => {
const instructions = 'You are a helpful assistant with tools';

Expand Down Expand Up @@ -64,6 +108,57 @@ describe('Agent', () => {
expect(agentTools.getTool2?.description).toBe('Second test tool');
});

it('should wait for realtime session chat context updates', async () => {
const agent = new Agent({ instructions: 'test' });
const activity = Object.create(AgentActivity.prototype) as AgentActivity & {
agent: Agent;
realtimeSession: { updateChatCtx: ReturnType<typeof vi.fn> };
};
const chatCtx = new ChatContext();
let resolveUpdate: () => void = () => {
throw new Error('update promise was not initialized');
};
const updatePromise = new Promise<void>((resolve) => {
resolveUpdate = resolve;
});
activity.agent = agent;
activity.realtimeSession = {
updateChatCtx: vi.fn(() => updatePromise),
};

let settled = false;
const update = activity.updateChatCtx(chatCtx).then(() => {
settled = true;
});

await Promise.resolve();

expect(activity.realtimeSession.updateChatCtx).toHaveBeenCalledOnce();
expect(settled).toBe(false);

resolveUpdate();
await update;

expect(settled).toBe(true);
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

it('should propagate realtime session chat context update failures', async () => {
const agent = new Agent({ instructions: 'test' });
const activity = Object.create(AgentActivity.prototype) as AgentActivity & {
agent: Agent;
realtimeSession: { updateChatCtx: ReturnType<typeof vi.fn> };
};
const chatCtx = new ChatContext();
const error = new Error('realtime update failed');
activity.agent = agent;
activity.realtimeSession = {
updateChatCtx: vi.fn(() => Promise.reject(error)),
};

await expect(activity.updateChatCtx(chatCtx)).rejects.toBe(error);
expect(activity.realtimeSession.updateChatCtx).toHaveBeenCalledOnce();
});

it('should return a copy of tools, not the original reference', () => {
const instructions = 'You are a helpful assistant';
const mockTool = tool({
Expand Down
2 changes: 1 addition & 1 deletion agents/src/voice/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export class Agent<UserData = any> {
return;
}

this._agentActivity.updateChatCtx(chatCtx);
await this._agentActivity.updateChatCtx(chatCtx);
}

// TODO: Add when AgentConfigUpdate is ported to ChatContext.
Expand Down
2 changes: 1 addition & 1 deletion agents/src/voice/agent_activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ export class AgentActivity implements RecognitionHooks {

if (this.realtimeSession) {
removeInstructions(chatCtx);
this.realtimeSession.updateChatCtx(chatCtx);
await this.realtimeSession.updateChatCtx(chatCtx);
} else {
updateInstructions({
chatCtx,
Expand Down