Skip to content
Closed
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/console-server-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents': minor
---

Send agent server info to the LiveKit CLI host when starting a console session.
4 changes: 4 additions & 0 deletions agents/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,15 @@ export const runApp = (opts: ServerOptions) => {
.action((...[, command]) => {
const commandOptions = command.opts();
opts.logLevel = commandOptions.logLevel;
const globalOptions = program.optsWithGlobals();
opts.wsURL = globalOptions.url || opts.wsURL;
initializeLogger({ pretty: true, level: opts.logLevel });
process.env.LIVEKIT_DEV_MODE = '1';
runConsole({
agentPath: opts.agent,
connectAddr: commandOptions.connectAddr,
agentName: opts.agentName,
wsURL: opts.wsURL,
record: commandOptions.record === true,
}).catch((error) => {
log().fatal(`console mode failed: ${formatErrorMessage(error)}`);
Expand Down
11 changes: 10 additions & 1 deletion agents/src/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ async function loadAgent(agentPath: string): Promise<Agent> {
export async function runConsole({
agentPath,
connectAddr,
agentName,
wsURL,
record,
}: {
agentPath: string;
connectAddr: string;
agentName: string;
wsURL: string;
record: boolean;
}): Promise<void> {
const logger = log();
Expand All @@ -77,7 +81,12 @@ export async function runConsole({
const agent = await loadAgent(agentPath);
const prewarm = agent.prewarm ?? defaultInitializeProcessFunc;

const transport = new TcpSessionTransport(host, port);
const transport = new TcpSessionTransport(host, port, {
serverInfo: {
agentName,
url: wsURL,
},
});
const audioInput = new TcpAudioInput();
const audioOutput = new TcpAudioOutput(transport);

Expand Down
32 changes: 29 additions & 3 deletions agents/src/voice/remote_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0
import { Duration, Timestamp } from '@bufbuild/protobuf';
import { AgentSession as pb } from '@livekit/protocol';
import { AgentDev, AgentSession as pb } from '@livekit/protocol';
import type { ByteStreamReader, Room, TextStreamInfo } from '@livekit/rtc-node';
import { ThrowsPromise } from '@livekit/throws-transformer/throws';
import type { TypedEventEmitter } from '@livekit/typed-emitter';
Expand Down Expand Up @@ -241,6 +241,13 @@ const TCP_HEADER_SIZE = 4;
const TCP_MAX_MESSAGE_SIZE = 1 << 20; // 1 MiB
const TCP_DRAIN_THRESHOLD = 64 * 1024; // 64 KiB

type TcpSessionTransportOptions = {
serverInfo?: {
agentName: string;
url: string;
};
};

/**
* Resolve once the socket's write buffer drains, or when the socket closes or
* errors. Unlike a bare `once('drain')`, this can't hang forever if the peer
Expand Down Expand Up @@ -268,13 +275,19 @@ function waitForDrain(socket: net.Socket): Promise<void> {
export class TcpSessionTransport extends SessionTransport {
private readonly host: string;
private readonly port: number;
private readonly options: TcpSessionTransportOptions;
private socket: net.Socket | null = null;
private closed = false;

constructor(host: string, port: number) {
constructor(
host: string,
port: number,
options: { serverInfo?: { agentName: string; url: string } } = {},
) {
super();
this.host = host;
this.port = port;
this.options = options;
}

override async start(): Promise<void> {
Expand All @@ -291,13 +304,26 @@ export class TcpSessionTransport extends SessionTransport {
};
socket.once('error', onConnectError);
});

if (this.options.serverInfo) {
const msg = new AgentDev.AgentDevMessage({
message: {
case: 'serverInfo',
value: new AgentDev.ServerInfo(this.options.serverInfo),
},
});
await this.sendFramed(msg.toBinary());
}
Comment on lines +308 to +316

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.

🔍 Server info handshake uses a different protobuf type than session messages on the same TCP stream

The start() method at agents/src/voice/remote_session.ts:308-316 sends an AgentDev.AgentDevMessage over the same length-prefixed TCP stream that subsequently carries pb.AgentSessionMessage frames. The broker (LiveKit CLI host) must know to parse the first frame as AgentDevMessage rather than AgentSessionMessage. This is a protocol-level contract that isn't enforced or documented in this codebase — correctness depends on the broker implementation matching this expectation. If the broker doesn't handle this, the first frame will be silently misinterpreted or cause a parse error.

Open in Devin Review

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

}

override async sendMessage(msg: pb.AgentSessionMessage): Promise<void> {
await this.sendFramed(msg.toBinary());
}

private async sendFramed(data: Uint8Array): Promise<void> {
const socket = this.socket;
if (this.closed || socket === null) return;

const data = msg.toBinary();
const header = Buffer.allocUnsafe(TCP_HEADER_SIZE);
header.writeUInt32BE(data.length, 0);
const flushed = socket.write(Buffer.concat([header, data]));
Expand Down
Loading