From 101e773ec62c1dcbf409f5a593f9c46d9ed5d8db Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Jul 2026 00:29:47 +0200 Subject: [PATCH 1/5] actually implement CLI protocol --- agents/package.json | 2 +- agents/src/cli.ts | 23 ++++++++++++- agents/src/cli_client.ts | 73 ++++++++++++++++++++++++++++++++++++++++ pnpm-lock.yaml | 12 +++---- 4 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 agents/src/cli_client.ts diff --git a/agents/package.json b/agents/package.json index 330e5b01a..f82994897 100644 --- a/agents/package.json +++ b/agents/package.json @@ -55,7 +55,7 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@livekit/local-inference": "^0.2.6", "@livekit/mutex": "^1.1.1", - "@livekit/protocol": "^1.48.0", + "@livekit/protocol": "^1.49.0", "@livekit/throws-transformer": "0.1.8", "@livekit/typed-emitter": "^3.0.0", "@opentelemetry/api": "^1.9.0", diff --git a/agents/src/cli.ts b/agents/src/cli.ts index 00b2c7813..713550530 100644 --- a/agents/src/cli.ts +++ b/agents/src/cli.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Command, Option } from 'commander'; import type { EventEmitter } from 'node:events'; +import { CLIClient } from './cli_client.js'; import { runConsole } from './console.js'; import { type PluginDownloadFailure, formatDownloadFailureMessage } from './download.js'; import { initializeLogger, log } from './log.js'; @@ -19,6 +20,8 @@ type CliArgs = { event?: EventEmitter; room?: string; participantIdentity?: string; + // Address of the driving `lk` CLI's dev channel (set by `lk agent dev`). + cliAddr?: string; }; const formatErrorMessage = (error: unknown): string => @@ -30,7 +33,15 @@ const runServer = async (args: CliArgs) => { // though `production` is defined in ServerOptions, it will always be overridden by CLI. const { production: _, ...opts } = args.opts; // eslint-disable-line @typescript-eslint/no-unused-vars - const server = new AgentServer(new ServerOptions({ ...opts, production: args.production })); + const serverOptions = new ServerOptions({ ...opts, production: args.production }); + const server = new AgentServer(serverOptions); + + // When launched by `lk agent dev`, report ServerInfo over the CLI's dev channel + // so it can surface e.g. a Cloud console link. Best-effort; never fatal. + const cliClient = args.cliAddr + ? new CLIClient(args.cliAddr, serverOptions.agentName, serverOptions.wsURL) + : undefined; + cliClient?.start(); if (args.room) { server.event.once('worker_registered', () => { @@ -53,6 +64,7 @@ const runServer = async (args: CliArgs) => { logger.error(e); } } + cliClient?.close(); await server.close(); logger.debug('worker closed due to SIGINT.'); process.exit(130); // SIGINT exit code @@ -67,6 +79,7 @@ const runServer = async (args: CliArgs) => { logger.error(e); } } + cliClient?.close(); await server.close(); logger.debug('worker closed due to SIGTERM.'); process.exit(143); // SIGTERM exit code @@ -77,6 +90,8 @@ const runServer = async (args: CliArgs) => { } catch { logger.fatal('closing worker due to error.'); process.exit(1); + } finally { + cliClient?.close(); } }; @@ -163,6 +178,11 @@ export const runApp = (opts: ServerOptions) => { .command('dev') .description('Start the worker in development mode') .addOption(logLevelOption('debug')) + .addOption( + // Set by `lk agent dev`: address of the CLI's dev channel the agent reports + // ServerInfo to (agent name + URL, e.g. for a Cloud console link). + new Option('--cli-addr ', 'Internal use only').hideHelp(), + ) .action((...[, command]) => { const globalOptions = program.optsWithGlobals(); const commandOptions = command.opts(); @@ -176,6 +196,7 @@ export const runApp = (opts: ServerOptions) => { opts, production: false, watch: false, + cliAddr: commandOptions.cliAddr, }); }); diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts new file mode 100644 index 000000000..000a352a5 --- /dev/null +++ b/agents/src/cli_client.ts @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { AgentDev } from '@livekit/protocol'; +import { type Socket, createConnection } from 'node:net'; +import { log } from './log.js'; + +/** + * Client for the dev channel opened by the driving `lk` CLI (its address is + * passed via `--cli-addr`). On connect it reports this agent server's + * {@link AgentDev.ServerInfo | ServerInfo} (agent name + LiveKit URL) so the CLI + * can surface things like a Cloud console link. + * + * This mirrors the Python `WatchClient`'s ServerInfo handshake. Unlike Python, + * Node reloads are a plain kill+respawn, so there is no running-job capture / + * restore — the client only sends ServerInfo. It is strictly best-effort: a + * missing or broken dev channel must never take the agent down. + */ +export class CLIClient { + #cliAddr: string; + #agentName: string; + #url: string; + #socket?: Socket; + + constructor(cliAddr: string, agentName: string, url: string) { + this.#cliAddr = cliAddr; + this.#agentName = agentName; + this.#url = url; + } + + start(): void { + const logger = log().child({ cliAddr: this.#cliAddr }); + + // cli_addr is host:port; the host may itself contain ':' (IPv6), so split on + // the last colon. + const sep = this.#cliAddr.lastIndexOf(':'); + if (sep === -1) { + logger.warn('invalid --cli-addr, skipping dev channel'); + return; + } + const host = this.#cliAddr.slice(0, sep); + const port = Number.parseInt(this.#cliAddr.slice(sep + 1), 10); + + const socket = createConnection({ host, port }, () => { + const msg = new AgentDev.AgentDevMessage({ + message: { + case: 'serverInfo', + value: new AgentDev.ServerInfo({ agentName: this.#agentName, url: this.#url }), + }, + }); + this.#sendProto(socket, msg.toBinary()); + }); + + // Best-effort: log and move on if the CLI isn't listening. + socket.on('error', (err) => { + logger.debug(`dev channel unavailable: ${err.message}`); + }); + + this.#socket = socket; + } + + close(): void { + this.#socket?.destroy(); + this.#socket = undefined; + } + + /** Frames a message with a 4-byte big-endian length prefix (matches the Go CLI). */ + #sendProto(socket: Socket, payload: Uint8Array): void { + const header = Buffer.allocUnsafe(4); + header.writeUInt32BE(payload.length, 0); + socket.write(Buffer.concat([header, payload])); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f00f5059b..34cab48dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,8 +119,8 @@ importers: specifier: ^1.1.1 version: 1.1.1 '@livekit/protocol': - specifier: ^1.48.0 - version: 1.48.2 + specifier: ^1.49.0 + version: 1.49.0 '@livekit/throws-transformer': specifier: 0.1.8 version: 0.1.8(typescript@5.9.3) @@ -2225,8 +2225,8 @@ packages: cpu: [x64] os: [win32] - '@livekit/protocol@1.48.2': - resolution: {integrity: sha512-xFcZdiVa4LpykarDZwdXbnS17qq+qbNKIT9v5/peNFNTjnMalmJkyo+XIIYfix6T9pG4LumjNzNBvxPEkIOrBg==} + '@livekit/protocol@1.49.0': + resolution: {integrity: sha512-TCmihwjHBWAUdNrqvmrN/K59r7vDnPIy3OEaE3p0g4FvdcQTiLynmFC3igR25Q2fnifahhAJjnnfSCtWUK7kYg==} '@livekit/rtc-ffi-bindings-darwin-arm64@0.12.60': resolution: {integrity: sha512-YHXqybkYfaTc3txJXXWoVogiSP3yKJdkaZlIlZ6IDMGnN9elUoHDYU+ZSn/rbdGu0pp4HUOzffXkbkItN735Bw==} @@ -5958,7 +5958,7 @@ snapshots: '@livekit/noise-cancellation-win32-x64@0.1.9': optional: true - '@livekit/protocol@1.48.2': + '@livekit/protocol@1.49.0': dependencies: '@bufbuild/protobuf': 1.10.1 @@ -8172,7 +8172,7 @@ snapshots: livekit-server-sdk@2.14.1: dependencies: '@bufbuild/protobuf': 1.10.1 - '@livekit/protocol': 1.48.2 + '@livekit/protocol': 1.49.0 camelcase-keys: 9.1.3 jose: 5.2.4 From dcf8eefdf76a855c513426dd3de69b8f26306a62 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Thu, 9 Jul 2026 15:36:00 -0700 Subject: [PATCH 2/5] Create shaky-flowers-allow.md --- .changeset/shaky-flowers-allow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaky-flowers-allow.md diff --git a/.changeset/shaky-flowers-allow.md b/.changeset/shaky-flowers-allow.md new file mode 100644 index 000000000..0191beaf9 --- /dev/null +++ b/.changeset/shaky-flowers-allow.md @@ -0,0 +1,5 @@ +--- +"@livekit/agents": patch +--- + +implement CLI serverInfo protocol From 0a0dfc21beb8fc197046d862acc1d5a7273cb29e Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Jul 2026 04:58:12 -0700 Subject: [PATCH 3/5] Update agents/src/cli_client.ts Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- agents/src/cli_client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts index 000a352a5..c4636b5c3 100644 --- a/agents/src/cli_client.ts +++ b/agents/src/cli_client.ts @@ -38,7 +38,10 @@ export class CLIClient { logger.warn('invalid --cli-addr, skipping dev channel'); return; } - const host = this.#cliAddr.slice(0, sep); + let host = this.#cliAddr.slice(0, sep); + if (host.startsWith('[') && host.endsWith(']')) { + host = host.slice(1, -1); + } const port = Number.parseInt(this.#cliAddr.slice(sep + 1), 10); const socket = createConnection({ host, port }, () => { From c32326f1d418f0a57bd6010fa1e2704fd8913aab Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Jul 2026 05:12:00 -0700 Subject: [PATCH 4/5] Update agents/src/cli_client.ts Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- agents/src/cli_client.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts index c4636b5c3..4cfe9f029 100644 --- a/agents/src/cli_client.ts +++ b/agents/src/cli_client.ts @@ -43,16 +43,21 @@ export class CLIClient { host = host.slice(1, -1); } const port = Number.parseInt(this.#cliAddr.slice(sep + 1), 10); + if (Number.isNaN(port) || port < 0 || port > 65535) { + logger.warn('invalid port in --cli-addr, skipping dev channel'); + return; + } - const socket = createConnection({ host, port }, () => { - const msg = new AgentDev.AgentDevMessage({ - message: { - case: 'serverInfo', - value: new AgentDev.ServerInfo({ agentName: this.#agentName, url: this.#url }), - }, + try { + const socket = createConnection({ host, port }, () => { + const msg = new AgentDev.AgentDevMessage({ + message: { + case: 'serverInfo', + value: new AgentDev.ServerInfo({ agentName: this.#agentName, url: this.#url }), + }, + }); + this.#sendProto(socket, msg.toBinary()); }); - this.#sendProto(socket, msg.toBinary()); - }); // Best-effort: log and move on if the CLI isn't listening. socket.on('error', (err) => { From 37a615357b6c264420541e4aa601dce707b6396b Mon Sep 17 00:00:00 2001 From: David Zhao Date: Fri, 10 Jul 2026 15:28:27 +0200 Subject: [PATCH 5/5] fix: close unterminated try block in CLIClient.start The previous commit added a try block around createConnection but never closed it, breaking the build with a syntax error. Co-Authored-By: Claude Fable 5 --- agents/src/cli_client.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/agents/src/cli_client.ts b/agents/src/cli_client.ts index 4cfe9f029..7942e4c32 100644 --- a/agents/src/cli_client.ts +++ b/agents/src/cli_client.ts @@ -59,12 +59,15 @@ export class CLIClient { this.#sendProto(socket, msg.toBinary()); }); - // Best-effort: log and move on if the CLI isn't listening. - socket.on('error', (err) => { - logger.debug(`dev channel unavailable: ${err.message}`); - }); + // Best-effort: log and move on if the CLI isn't listening. + socket.on('error', (err) => { + logger.debug(`dev channel unavailable: ${err.message}`); + }); - this.#socket = socket; + this.#socket = socket; + } catch (err) { + logger.debug(`dev channel unavailable: ${(err as Error).message}`); + } } close(): void {