From 36e306f1bc6b307feb83eb92bfcddf6efda80f09 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Thu, 5 Mar 2026 05:04:45 +0000 Subject: [PATCH 01/15] update --- examples/src/phonic_realtime_agent.ts | 15 +++++- plugins/phonic/src/realtime/realtime_model.ts | 46 ++++++++++++------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index 2102fab2a..59e92bd10 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -32,8 +32,9 @@ export default defineAgent({ // Uses PHONIC_API_KEY environment variable when apiKey is not provided llm: new phonic.realtime.RealtimeModel({ voice: 'sabrina', - welcomeMessage: 'Hey there, how can I help you today?', + // welcomeMessage: 'Hey there, how can I help you today?', audioSpeed: 1.2, + baseUrl: 'http://localhost:3520', }), }); @@ -43,7 +44,17 @@ export default defineAgent({ }); await ctx.connect(); + + // await session.generateReply({ + // instructions: 'greet the user and ask about their day', + // }); + + // setInterval(async () => { + // await session.generateReply({ + // instructions: 'tell the user that you like Port of Parity Parity, which is a restaurant in san francisco', + // }); + // }, 15 * 1000); }, }); -cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) })); +cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url), agentName: "qiong-generate-1" })); diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 6baf75499..cf22744fe 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -261,7 +261,7 @@ export class RealtimeSession extends llm.RealtimeSession { 'updateChatCtx called but no new tool call outputs to send. Phonic does not support general chat context updates.', ); } else { - this.startNewAssistantTurn(); + this.startNewAssistantTurn({ userInitiated: false }); } } @@ -322,11 +322,20 @@ export class RealtimeSession extends llm.RealtimeSession { } } - // TODO @Phonic-Co: Implement generateReply - async generateReply(_instructions?: string): Promise { - throw new Error( - 'generateReply is not yet supported by the Phonic realtime model. Consider using `welcomeMessage` instead.', - ); + async generateReply(instructions?: string): Promise { + const systemMessage = instructions ?? 'Please say something.' + const payload = { type: 'generate_reply' as const, system_message: systemMessage }; + + // temp + if (this.socket) { + const ws = this.socket.socket; + ws?.send(JSON.stringify(payload)); + } else { + this.logger.warn('Cannot send generate_reply: WebSocket not available'); + } + + this.closeCurrentGeneration({ interrupted: false }); + return this.startNewAssistantTurn({ userInitiated: true }); } async commitAudio(): Promise { @@ -412,7 +421,9 @@ export class RealtimeSession extends llm.RealtimeSession { switch (message.type) { case 'assistant_started_speaking': - this.startNewAssistantTurn(); + if (this.currentGeneration === undefined) { + this.startNewAssistantTurn({ userInitiated: false }); + } break; case 'assistant_finished_speaking': this.finishAssistantTurn(); @@ -469,7 +480,7 @@ export class RealtimeSession extends llm.RealtimeSession { */ if (this.currentGeneration === undefined && message.text) { this.logger.debug('Starting new generation due to text in audio chunk'); - this.startNewAssistantTurn(); + this.startNewAssistantTurn({ userInitiated: false }); } const gen = this.currentGeneration; @@ -521,7 +532,7 @@ export class RealtimeSession extends llm.RealtimeSession { if (this.currentGeneration === undefined) { this.logger.warn('Encountered tool call but no active generation. Starting new turn.'); - this.startNewAssistantTurn(); + this.startNewAssistantTurn({ userInitiated: false }); } this.currentGeneration!.functionChannel.write( @@ -553,11 +564,9 @@ export class RealtimeSession extends llm.RealtimeSession { }); } - private startNewAssistantTurn(): void { - if (this.currentGeneration) { - this.closeCurrentGeneration({ interrupted: true }); - } - + private startNewAssistantTurn(options: { + userInitiated: boolean; + }): llm.GenerationCreatedEvent { const responseId = shortuuid('PS_'); const textChannel = stream.createStreamChannel(); @@ -581,12 +590,15 @@ export class RealtimeSession extends llm.RealtimeSession { outputText: '', }; - this.emit('generation_created', { + const generationEvent: llm.GenerationCreatedEvent = { messageStream: messageChannel.stream(), functionStream: functionChannel.stream(), - userInitiated: false, + userInitiated: options.userInitiated, responseId, - }); + }; + + this.emit('generation_created', generationEvent); + return generationEvent; } private finishAssistantTurn(): void { From b45bee7915be6f8bdf78793e7658fffdceb0d7a8 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 01:46:11 +0000 Subject: [PATCH 02/15] Update sdk --- examples/src/phonic_realtime_agent.ts | 12 +++--------- plugins/phonic/package.json | 2 +- plugins/phonic/src/realtime/realtime_model.ts | 7 +------ pnpm-lock.yaml | 10 +++++----- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index 59e92bd10..b3a3b46cf 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -45,15 +45,9 @@ export default defineAgent({ await ctx.connect(); - // await session.generateReply({ - // instructions: 'greet the user and ask about their day', - // }); - - // setInterval(async () => { - // await session.generateReply({ - // instructions: 'tell the user that you like Port of Parity Parity, which is a restaurant in san francisco', - // }); - // }, 15 * 1000); + await session.generateReply({ + instructions: 'Greet the user, asking about their day.', + }); }, }); diff --git a/plugins/phonic/package.json b/plugins/phonic/package.json index 219af9fae..6e06b2412 100644 --- a/plugins/phonic/package.json +++ b/plugins/phonic/package.json @@ -41,7 +41,7 @@ "typescript": "^5.0.0" }, "dependencies": { - "phonic": "^0.30.39" + "phonic": "^0.31.2" }, "peerDependencies": { "@livekit/agents": "workspace:*", diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index cf22744fe..245f18165 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -323,13 +323,8 @@ export class RealtimeSession extends llm.RealtimeSession { } async generateReply(instructions?: string): Promise { - const systemMessage = instructions ?? 'Please say something.' - const payload = { type: 'generate_reply' as const, system_message: systemMessage }; - - // temp if (this.socket) { - const ws = this.socket.socket; - ws?.send(JSON.stringify(payload)); + this.socket.sendGenerateReply({ type: 'generate_reply', system_message: instructions }); } else { this.logger.warn('Cannot send generate_reply: WebSocket not available'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94cda9f29..c8a126114 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -752,8 +752,8 @@ importers: plugins/phonic: dependencies: phonic: - specifier: ^0.30.39 - version: 0.30.39 + specifier: ^0.31.2 + version: 0.31.2 devDependencies: '@livekit/agents': specifier: workspace:* @@ -4393,8 +4393,8 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} - phonic@0.30.39: - resolution: {integrity: sha512-4XcTtzt9Q32GkufSilpPl+6qyTpgJasjUKz4tMfuQ65VHglOygDcMISbmDwKuFJSJUURb1l8T0Kw5GnEXSHCFA==} + phonic@0.31.2: + resolution: {integrity: sha512-vpFBFpJYCjgqG2RPe+z3EmwLu/HN8CX1A9kEvNestpm/PWhaGLcEL3ySZmm+AOCaDeRykdhdJ5smEFKj20gd4Q==} engines: {node: '>=18.0.0'} picocolors@1.0.1: @@ -8958,7 +8958,7 @@ snapshots: pathval@2.0.0: {} - phonic@0.30.39: + phonic@0.31.2: dependencies: ws: 8.18.3 transitivePeerDependencies: From 9b21cb0bb16256254e7c9cf5a2130e5cc5aa6d97 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 02:13:56 +0000 Subject: [PATCH 03/15] cleanup --- examples/src/phonic_realtime_agent.ts | 4 +--- plugins/phonic/README.md | 10 +++++++++- plugins/phonic/src/realtime/realtime_model.ts | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index b3a3b46cf..660ad9277 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -32,9 +32,7 @@ export default defineAgent({ // Uses PHONIC_API_KEY environment variable when apiKey is not provided llm: new phonic.realtime.RealtimeModel({ voice: 'sabrina', - // welcomeMessage: 'Hey there, how can I help you today?', audioSpeed: 1.2, - baseUrl: 'http://localhost:3520', }), }); @@ -51,4 +49,4 @@ export default defineAgent({ }, }); -cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url), agentName: "qiong-generate-1" })); +cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) })); diff --git a/plugins/phonic/README.md b/plugins/phonic/README.md index 4b7bec8a4..f58c25b09 100644 --- a/plugins/phonic/README.md +++ b/plugins/phonic/README.md @@ -5,6 +5,9 @@ Realtime voice AI integration for [Phonic](https://phonic.co/) with LiveKit Agen ## Usage ```typescript +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 import { type JobContext, ServerOptions, cli, defineAgent, llm, voice } from '@livekit/agents'; import * as phonic from '@livekit/agents-plugin-phonic'; import { fileURLToPath } from 'node:url'; @@ -18,6 +21,7 @@ const toggleLight = llm.tool({ }), execute: async ({ light_id, state }) => { console.log(`Turning ${state} light ${light_id}`); + await new Promise((resolve) => setTimeout(resolve, 1_000)); return `Light ${light_id} turned ${state}`; }, }); @@ -35,7 +39,6 @@ export default defineAgent({ // Uses PHONIC_API_KEY environment variable when apiKey is not provided llm: new phonic.realtime.RealtimeModel({ voice: 'sabrina', - welcomeMessage: 'Hey there, how can I help you today?', audioSpeed: 1.2, }), }); @@ -46,10 +49,15 @@ export default defineAgent({ }); await ctx.connect(); + + await session.generateReply({ + instructions: 'Greet the user, asking about their day.', + }); }, }); cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) })); + ``` ## Configuration diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 245f18165..d10ca1478 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -559,7 +559,9 @@ export class RealtimeSession extends llm.RealtimeSession { }); } - private startNewAssistantTurn(options: { + private startNewAssistantTurn({ + userInitiated, + }: { userInitiated: boolean; }): llm.GenerationCreatedEvent { const responseId = shortuuid('PS_'); @@ -588,7 +590,7 @@ export class RealtimeSession extends llm.RealtimeSession { const generationEvent: llm.GenerationCreatedEvent = { messageStream: messageChannel.stream(), functionStream: functionChannel.stream(), - userInitiated: options.userInitiated, + userInitiated, responseId, }; From da0631c2e29ced8846e4f97c4f29d7e0333f2220 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 03:30:14 +0000 Subject: [PATCH 04/15] Add changeset --- .changeset/curvy-rooms-agree.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curvy-rooms-agree.md diff --git a/.changeset/curvy-rooms-agree.md b/.changeset/curvy-rooms-agree.md new file mode 100644 index 000000000..16464796d --- /dev/null +++ b/.changeset/curvy-rooms-agree.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents-plugin-phonic': patch +--- + +Add generateReply support to Phonic plugin From 7539f78c3b70ecf371b304cc558098e481277cc4 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 04:11:11 +0000 Subject: [PATCH 05/15] Add back safety check --- plugins/phonic/src/realtime/realtime_model.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index d10ca1478..9c5894a5e 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -564,6 +564,10 @@ export class RealtimeSession extends llm.RealtimeSession { }: { userInitiated: boolean; }): llm.GenerationCreatedEvent { + if (this.currentGeneration) { + this.closeCurrentGeneration({ interrupted: true }); + } + const responseId = shortuuid('PS_'); const textChannel = stream.createStreamChannel(); From 9a2a6ea6650ebf5d143649549da09b53bd50de8e Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 18:18:03 +0000 Subject: [PATCH 06/15] Allow updating chat context prior to config senting and to add system messages --- plugins/phonic/src/realtime/realtime_model.ts | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 9c5894a5e..2428468f6 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -202,7 +202,9 @@ export class RealtimeSession extends llm.RealtimeSession { private connectTask: Promise; private toolDefinitions: Record[] = []; private pendingToolCallIds = new Set(); + private lastAgentChatCtx = llm.ChatContext.empty(); private readyToStart = false; + private systemPromptPostfix = ''; constructor(realtimeModel: RealtimeModel) { super(realtimeModel); @@ -243,9 +245,21 @@ export class RealtimeSession extends llm.RealtimeSession { } async updateChatCtx(chatCtx: llm.ChatContext): Promise { - let sent = false; - for (const item of chatCtx.items) { - if (item.type === 'function_call_output' && this.pendingToolCallIds.has(item.callId)) { + if (!this.configSent) { + if (chatCtx.items.length > 0) { + this.logger.debug("updateChatCtx called prior to config being sent to Phonic. Including conversation state in system instructions.") + const turnHistory = chatCtx.items.filter((item) => item.type === 'message').map((item) => `${item.role}: ${item.content.join('\n')}`).join('\n'); + this.systemPromptPostfix = "This conversation is being continued from an existing conversation. The following is the conversation history. You are the assistant speaking to the user." + turnHistory + } + } + + const diffOps = llm.computeChatCtxDiff(this._chatCtx, chatCtx); + let sentToolCallOutput = false; + let sentAddSystemMessage = false; + + for (const [, itemId] of diffOps.toCreate) { + const item = chatCtx.getById(itemId); + if (item?.type === 'function_call_output' && this.pendingToolCallIds.has(item.callId)) { this.pendingToolCallIds.delete(item.callId); this.logger.info(`Sending tool call output for ${item.name} (call_id: ${item.callId})`); this.socket?.sendToolCallOutput({ @@ -253,14 +267,25 @@ export class RealtimeSession extends llm.RealtimeSession { tool_call_id: item.callId, output: item.output, }); - sent = true; + sentToolCallOutput = true; + } + if (item?.type === 'message' && item.role === 'system' && typeof item.content === 'string') { + this.socket?.sendAddSystemMessage({ + type: 'add_system_message', + system_message: item.content, + }); + sentAddSystemMessage = true; } } - if (!sent) { + + this._chatCtx = chatCtx.copy() + + if (!sentToolCallOutput && !sentAddSystemMessage) { this.logger.warn( 'updateChatCtx called but no new tool call outputs to send. Phonic does not support general chat context updates.', ); - } else { + } + if (sentToolCallOutput) { this.startNewAssistantTurn({ userInitiated: false }); } } @@ -394,7 +419,7 @@ export class RealtimeSession extends llm.RealtimeSession { project: this.options.project, welcome_message: this.options.welcomeMessage, generate_welcome_message: this.options.generateWelcomeMessage, - system_prompt: this.options.instructions, + system_prompt: this.options.instructions + this.systemPromptPostfix, voice_id: this.options.voice, input_format: 'pcm_44100', output_format: 'pcm_44100', From 17fe458caf21fb8aef5cf311829b176d861b2900 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 18:18:47 +0000 Subject: [PATCH 07/15] Allow developer --- plugins/phonic/src/realtime/realtime_model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 2428468f6..9a1bffd93 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -269,7 +269,7 @@ export class RealtimeSession extends llm.RealtimeSession { }); sentToolCallOutput = true; } - if (item?.type === 'message' && item.role === 'system' && typeof item.content === 'string') { + if (item?.type === 'message' && (item.role === 'system' || item.role === 'developer') && typeof item.content === 'string') { this.socket?.sendAddSystemMessage({ type: 'add_system_message', system_message: item.content, From 75dda35decdafba0bd6edfceb919c4a51d541df1 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 19:11:00 +0000 Subject: [PATCH 08/15] fixes --- examples/src/phonic_realtime_agent.ts | 14 +++++++++++++ plugins/phonic/src/realtime/realtime_model.ts | 20 +++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index 660ad9277..fac2fea61 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -46,6 +46,20 @@ export default defineAgent({ await session.generateReply({ instructions: 'Greet the user, asking about their day.', }); + + setTimeout(async () => { + const agent = session.currentAgent; + const chatCtx = agent.chatCtx.copy(); + chatCtx.addMessage({ + role: 'system', + content: + 'You really like the Port of Peri Peri (restaurant in San Francisco). Let the user know that.', + }); + await agent.updateChatCtx(chatCtx); + // session.generateReply({ + // instructions: 'Mention to the user that you really like the Port of Peri Peri restaurant.', + // }); + }, 10000); }, }); diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 9a1bffd93..1d75bf3b5 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -248,9 +248,10 @@ export class RealtimeSession extends llm.RealtimeSession { if (!this.configSent) { if (chatCtx.items.length > 0) { this.logger.debug("updateChatCtx called prior to config being sent to Phonic. Including conversation state in system instructions.") - const turnHistory = chatCtx.items.filter((item) => item.type === 'message').map((item) => `${item.role}: ${item.content.join('\n')}`).join('\n'); - this.systemPromptPostfix = "This conversation is being continued from an existing conversation. The following is the conversation history. You are the assistant speaking to the user." + turnHistory + const turnHistory = chatCtx.items.filter((item) => item.type === 'message' && 'textContent' in item).map((item) => `${item.role}: ${item.textContent}`).join('\n'); + this.systemPromptPostfix = "\n\nThis conversation is being continued from an existing conversation. You are the assistant speaking to the user. The following is the conversation history:\n" + turnHistory } + return; } const diffOps = llm.computeChatCtxDiff(this._chatCtx, chatCtx); @@ -269,12 +270,15 @@ export class RealtimeSession extends llm.RealtimeSession { }); sentToolCallOutput = true; } - if (item?.type === 'message' && (item.role === 'system' || item.role === 'developer') && typeof item.content === 'string') { - this.socket?.sendAddSystemMessage({ - type: 'add_system_message', - system_message: item.content, - }); - sentAddSystemMessage = true; + if (item?.type === 'message') { + if ((item.role === 'system' || item.role === 'developer') && item.textContent) { + this.logger.debug(`Sending add system message: ${item.textContent}`); + this.socket?.sendAddSystemMessage({ + type: 'add_system_message', + system_message: item.textContent, + }); + sentAddSystemMessage = true; + } } } From c1bf6ba8651772ff195292a32170c09c1c4ef1b2 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 19:12:36 +0000 Subject: [PATCH 09/15] remove --- examples/src/phonic_realtime_agent.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index fac2fea61..c80fca4a4 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -56,9 +56,6 @@ export default defineAgent({ 'You really like the Port of Peri Peri (restaurant in San Francisco). Let the user know that.', }); await agent.updateChatCtx(chatCtx); - // session.generateReply({ - // instructions: 'Mention to the user that you really like the Port of Peri Peri restaurant.', - // }); }, 10000); }, }); From 915142228c9a59c6bcbcdd89653ef7d36e2a3ab7 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Fri, 6 Mar 2026 19:32:37 +0000 Subject: [PATCH 10/15] format --- plugins/phonic/src/realtime/realtime_model.ts | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index 1d75bf3b5..ddff52ee7 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -247,9 +247,22 @@ export class RealtimeSession extends llm.RealtimeSession { async updateChatCtx(chatCtx: llm.ChatContext): Promise { if (!this.configSent) { if (chatCtx.items.length > 0) { - this.logger.debug("updateChatCtx called prior to config being sent to Phonic. Including conversation state in system instructions.") - const turnHistory = chatCtx.items.filter((item) => item.type === 'message' && 'textContent' in item).map((item) => `${item.role}: ${item.textContent}`).join('\n'); - this.systemPromptPostfix = "\n\nThis conversation is being continued from an existing conversation. You are the assistant speaking to the user. The following is the conversation history:\n" + turnHistory + this.logger.debug( + 'updateChatCtx called prior to config being sent to Phonic. Including conversation state in system instructions.', + ); + const turnHistory = chatCtx.items + .filter( + (item): item is llm.ChatMessage => + item.type === 'message' && + 'textContent' in item && + item.textContent !== undefined && + item.textContent.trim() !== '', + ) + .map((item) => `${item.role}: ${item.textContent}`) + .join('\n'); + this.systemPromptPostfix = + '\n\nThis conversation is being continued from an existing conversation. You are the assistant speaking to the user. The following is the conversation history:\n' + + turnHistory; } return; } @@ -257,7 +270,7 @@ export class RealtimeSession extends llm.RealtimeSession { const diffOps = llm.computeChatCtxDiff(this._chatCtx, chatCtx); let sentToolCallOutput = false; let sentAddSystemMessage = false; - + for (const [, itemId] of diffOps.toCreate) { const item = chatCtx.getById(itemId); if (item?.type === 'function_call_output' && this.pendingToolCallIds.has(item.callId)) { @@ -282,7 +295,7 @@ export class RealtimeSession extends llm.RealtimeSession { } } - this._chatCtx = chatCtx.copy() + this._chatCtx = chatCtx.copy(); if (!sentToolCallOutput && !sentAddSystemMessage) { this.logger.warn( From 1904d8e9bf839b514c52f41712ca4229b4c6616d Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Mon, 9 Mar 2026 18:37:19 +0000 Subject: [PATCH 11/15] Fixes --- plugins/phonic/src/realtime/realtime_model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index ddff52ee7..db42a523d 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -202,7 +202,6 @@ export class RealtimeSession extends llm.RealtimeSession { private connectTask: Promise; private toolDefinitions: Record[] = []; private pendingToolCallIds = new Set(); - private lastAgentChatCtx = llm.ChatContext.empty(); private readyToStart = false; private systemPromptPostfix = ''; @@ -263,6 +262,7 @@ export class RealtimeSession extends llm.RealtimeSession { this.systemPromptPostfix = '\n\nThis conversation is being continued from an existing conversation. You are the assistant speaking to the user. The following is the conversation history:\n' + turnHistory; + this._chatCtx = chatCtx.copy(); } return; } From 0ffb85ba68d7d0545fecf22df940611cdac5b909 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Mon, 9 Mar 2026 20:23:42 +0000 Subject: [PATCH 12/15] update example --- examples/src/phonic_realtime_agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/src/phonic_realtime_agent.ts b/examples/src/phonic_realtime_agent.ts index c80fca4a4..e873166d4 100644 --- a/examples/src/phonic_realtime_agent.ts +++ b/examples/src/phonic_realtime_agent.ts @@ -48,12 +48,13 @@ export default defineAgent({ }); setTimeout(async () => { + if (session.agentState === 'initializing') return; const agent = session.currentAgent; const chatCtx = agent.chatCtx.copy(); chatCtx.addMessage({ role: 'system', content: - 'You really like the Port of Peri Peri (restaurant in San Francisco). Let the user know that.', + "The user's name is Alex. He is from San Francisco and likes hiking. Use this information in your conversation.", }); await agent.updateChatCtx(chatCtx); }, 10000); From 95f91410ca0c38a9fa51a0021bddefb466a84864 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Mon, 9 Mar 2026 21:19:27 +0000 Subject: [PATCH 13/15] Add changeset --- .changeset/silly-rats-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-rats-walk.md diff --git a/.changeset/silly-rats-walk.md b/.changeset/silly-rats-walk.md new file mode 100644 index 000000000..af530f812 --- /dev/null +++ b/.changeset/silly-rats-walk.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents-plugin-phonic': patch +--- + +Add support for updateChatCtx to rehydrate conversations prior to conversation start to add system messages during an ongoing conversation From 9259fde46b60988b90269f0705b55237080c86d0 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Mon, 9 Mar 2026 21:28:20 +0000 Subject: [PATCH 14/15] require defined instructions --- plugins/phonic/src/realtime/realtime_model.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index db42a523d..d699e3f6c 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -428,6 +428,14 @@ export class RealtimeSession extends llm.RealtimeSession { await this.toolsReady.await; if (this.closed) return; + if (this.options.instructions === undefined) { + this.emitError( + new Error('Instructions are required to start a conversation with Phonic.'), + false, + ); + return; + } + this.configSent = true; this.socket.sendConfig({ type: 'config', From c8efe5cb6581161731cf413bdfd2d1ed36378372 Mon Sep 17 00:00:00 2001 From: Qiong Zhou Huang Date: Mon, 9 Mar 2026 21:42:23 +0000 Subject: [PATCH 15/15] guard against empty turns --- plugins/phonic/src/realtime/realtime_model.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/phonic/src/realtime/realtime_model.ts b/plugins/phonic/src/realtime/realtime_model.ts index d699e3f6c..47d712b6f 100644 --- a/plugins/phonic/src/realtime/realtime_model.ts +++ b/plugins/phonic/src/realtime/realtime_model.ts @@ -246,9 +246,6 @@ export class RealtimeSession extends llm.RealtimeSession { async updateChatCtx(chatCtx: llm.ChatContext): Promise { if (!this.configSent) { if (chatCtx.items.length > 0) { - this.logger.debug( - 'updateChatCtx called prior to config being sent to Phonic. Including conversation state in system instructions.', - ); const turnHistory = chatCtx.items .filter( (item): item is llm.ChatMessage => @@ -259,9 +256,14 @@ export class RealtimeSession extends llm.RealtimeSession { ) .map((item) => `${item.role}: ${item.textContent}`) .join('\n'); - this.systemPromptPostfix = - '\n\nThis conversation is being continued from an existing conversation. You are the assistant speaking to the user. The following is the conversation history:\n' + - turnHistory; + if (turnHistory.trim() !== '') { + this.logger.debug( + 'updateChatCtx called with messages prior to config being sent to Phonic. Including conversation state in system instructions.', + ); + this.systemPromptPostfix = + '\n\nThis conversation is being continued from an existing conversation. You are the assistant speaking to the user. The following is the conversation history:\n' + + turnHistory; + } this._chatCtx = chatCtx.copy(); } return;