Skip to content

Commit 11e7c80

Browse files
committed
fix(agent): resolve Node SDK version via the runtime, gate Node separately
The local-run version gate ran main's file-based CheckSDKVersion for Node too, which (a) couldn't find @livekit/agents in monorepo/workspace layouts where the dep is a `workspace:*` symlink rather than a versioned entry, and (b) gated on the Python thin-CLI baseline (1.6.0), the wrong floor for the Node SDK. Resolve the installed @livekit/agents via Node's own module resolution (embedded node_resolve_version.js), starting from the entrypoint's directory so pnpm/workspace symlinks and hoisting resolve exactly as they will at runtime, and gate Node on its own nodeAgentMinVersion. Python keeps the existing CheckSDKVersion path. Export IsVersionSatisfied for the reused comparison.
1 parent 781fbc3 commit 11e7c80

5 files changed

Lines changed: 100 additions & 20 deletions

File tree

cmd/lk/agent_run_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ func TestAgentProcessFailSignal(t *testing.T) {
3636
dir := t.TempDir()
3737
script := `console.log('shutting down job task {"reason": "job crashed"}'); setTimeout(() => {}, 30000);`
3838
require.NoError(t, os.WriteFile(filepath.Join(dir, "agent.js"), []byte(script), 0o644))
39-
// startAgent gates on the SDK version, so declare a satisfying dependency.
40-
pkgJSON := `{"dependencies": {"@livekit/agents": "1.6.0"}}`
41-
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(pkgJSON), 0o644))
39+
// startAgent gates on the SDK version, which Node resolves from node_modules,
40+
// so install a satisfying @livekit/agents stub.
41+
agentsDir := filepath.Join(dir, "node_modules", "@livekit", "agents")
42+
require.NoError(t, os.MkdirAll(agentsDir, 0o755))
43+
require.NoError(t, os.WriteFile(filepath.Join(agentsDir, "package.json"),
44+
[]byte(`{"name": "@livekit/agents", "version": "1.6.0"}`), 0o644))
4245

4346
ap, err := startAgent(AgentStartConfig{
4447
Dir: dir,

cmd/lk/node_resolve_version.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Reports the installed @livekit/agents version using Node's own module
2+
// resolution paths, so pnpm/workspace symlinks and hoisting resolve exactly as
3+
// they will at runtime. Reads package.json directly, so it works even before
4+
// the package is built. Prints the version to stdout, or exits non-zero if the
5+
// package can't be found.
6+
const { createRequire } = require('module');
7+
const path = require('path');
8+
const fs = require('fs');
9+
10+
try {
11+
const req = createRequire(path.join(process.cwd(), 'noop.js'));
12+
for (const base of req.resolve.paths('@livekit/agents') || []) {
13+
const pkgPath = path.join(base, '@livekit/agents', 'package.json');
14+
if (fs.existsSync(pkgPath)) {
15+
process.stdout.write(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '');
16+
process.exit(0);
17+
}
18+
}
19+
} catch (e) {}
20+
process.exit(1);

cmd/lk/simulate_subprocess.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717
import (
1818
"bufio"
1919
"context"
20+
_ "embed"
2021
"encoding/json"
2122
"fmt"
2223
"io"
@@ -129,6 +130,56 @@ func checkTypeStrippingSupport(dir, nodeBin string) error {
129130
return nil
130131
}
131132

133+
// nodeAgentMinVersion is the minimum @livekit/agents (agents-js) release the
134+
// CLI supports. Unlike the Python thin CLI (gated on thinCLIMinVersion), the
135+
// Node entrypoint exposes the start/console/simulate subcommands directly, so
136+
// the baseline differs. Local placeholder — the deploy path sources the
137+
// equivalent floor from server client settings.
138+
const nodeAgentMinVersion = "1.0.0"
139+
140+
// nodeResolveVersionScript asks Node to report the installed @livekit/agents
141+
// version using its own module resolution paths (so pnpm/workspace symlinks
142+
// and hoisting resolve exactly as they will at runtime). See the source file
143+
// for details.
144+
//
145+
//go:embed node_resolve_version.js
146+
var nodeResolveVersionScript string
147+
148+
// resolveNodeAgentVersion returns the installed @livekit/agents version as Node
149+
// resolves it from fromDir, or "" if it can't be determined.
150+
func resolveNodeAgentVersion(nodeBin, fromDir string) string {
151+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
152+
defer cancel()
153+
cmd := exec.CommandContext(ctx, nodeBin, "-e", nodeResolveVersionScript)
154+
cmd.Dir = fromDir
155+
out, err := cmd.Output()
156+
if err != nil {
157+
return ""
158+
}
159+
return strings.TrimSpace(string(out))
160+
}
161+
162+
// checkNodeSDKVersion gates a Node agent on nodeAgentMinVersion, resolving the
163+
// installed @livekit/agents from the entrypoint's directory so monorepo and
164+
// workspace layouts (where the dep is a workspace:* symlink, not a versioned
165+
// entry in the root package.json) report the version that will actually run.
166+
func checkNodeSDKVersion(cfg AgentStartConfig) error {
167+
nodeBin, err := findNodeBinary()
168+
if err != nil {
169+
return err
170+
}
171+
fromDir := filepath.Dir(filepath.Join(cfg.Dir, cfg.Entrypoint))
172+
version := resolveNodeAgentVersion(nodeBin, fromDir)
173+
if version == "" {
174+
return fmt.Errorf("@livekit/agents not found; install dependencies and make sure this is a LiveKit agent project")
175+
}
176+
// An unparseable version (e.g. a local "0.0.0-dev" tag) shouldn't block a run.
177+
if ok, err := agentfs.IsVersionSatisfied(version, nodeAgentMinVersion); err == nil && !ok {
178+
return fmt.Errorf("@livekit/agents version %s is too old, please upgrade to %s or newer", version, nodeAgentMinVersion)
179+
}
180+
return nil
181+
}
182+
132183
// defaultEntrypoints returns candidate entrypoint paths (relative to the
133184
// project root or working directory) probed for a project type, in priority
134185
// order. Forward slashes are valid on all platforms.
@@ -270,8 +321,14 @@ func buildAgentCommand(cfg AgentStartConfig) (string, []string, error) {
270321

271322
// startAgent launches a Python or Node agent subprocess and monitors its output.
272323
func startAgent(cfg AgentStartConfig) (*AgentProcess, error) {
273-
// fail fast when livekit-agents is older than the thin-CLI baseline
274-
if err := agentfs.CheckSDKVersion(cfg.Dir, cfg.ProjectType, map[string]string{
324+
// fail fast when the agent SDK is older than the baseline the CLI supports.
325+
// Node resolves the installed package via the runtime so workspace/monorepo
326+
// layouts work; Python parses project files against the thin-CLI baseline.
327+
if cfg.ProjectType.IsNode() {
328+
if err := checkNodeSDKVersion(cfg); err != nil {
329+
return nil, err
330+
}
331+
} else if err := agentfs.CheckSDKVersion(cfg.Dir, cfg.ProjectType, map[string]string{
275332
"python-min-sdk-version": thinCLIMinVersion,
276333
"node-min-sdk-version": thinCLIMinVersion,
277334
}); err != nil {

pkg/agentfs/sdk_version_check.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func checkRequirementsFile(filePath, minVersion string) VersionCheckResult {
205205

206206
version, found := parsePythonPackageVersion(line)
207207
if found {
208-
satisfied, err := isVersionSatisfied(version, minVersion)
208+
satisfied, err := IsVersionSatisfied(version, minVersion)
209209
return VersionCheckResult{
210210
PackageInfo: PackageInfo{
211211
Name: "livekit-agents",
@@ -243,7 +243,7 @@ func checkPyprojectToml(filePath, minVersion string) VersionCheckResult {
243243
if line, ok := dep.(string); ok {
244244
version, found := parsePythonPackageVersion(line)
245245
if found {
246-
satisfied, err := isVersionSatisfied(version, minVersion)
246+
satisfied, err := IsVersionSatisfied(version, minVersion)
247247
return VersionCheckResult{
248248
PackageInfo: PackageInfo{
249249
Name: "livekit-agents",
@@ -281,7 +281,7 @@ func checkPipfile(filePath, minVersion string) VersionCheckResult {
281281
version = "latest"
282282
}
283283

284-
satisfied, err := isVersionSatisfied(version, minVersion)
284+
satisfied, err := IsVersionSatisfied(version, minVersion)
285285
return VersionCheckResult{
286286
PackageInfo: PackageInfo{
287287
Name: "livekit-agents",
@@ -326,7 +326,7 @@ func checkSetupPy(filePath, minVersion string) VersionCheckResult {
326326
}
327327
version, found := parsePythonPackageVersion(packageLine)
328328
if found {
329-
satisfied, err := isVersionSatisfied(version, minVersion)
329+
satisfied, err := IsVersionSatisfied(version, minVersion)
330330
return VersionCheckResult{
331331
PackageInfo: PackageInfo{
332332
Name: "livekit-agents",
@@ -360,7 +360,7 @@ func checkSetupCfg(filePath, minVersion string) VersionCheckResult {
360360
if matches != nil {
361361
version := strings.TrimSpace(matches[2])
362362

363-
satisfied, err := isVersionSatisfied(version, minVersion)
363+
satisfied, err := IsVersionSatisfied(version, minVersion)
364364
return VersionCheckResult{
365365
PackageInfo: PackageInfo{
366366
Name: "livekit-agents",
@@ -407,7 +407,7 @@ func checkPackageJSON(filePath, minVersion string) VersionCheckResult {
407407

408408
for _, deps := range dependencyMaps {
409409
if version, ok := deps["@livekit/agents"]; ok {
410-
satisfied, err := isVersionSatisfied(version, minVersion)
410+
satisfied, err := IsVersionSatisfied(version, minVersion)
411411
return VersionCheckResult{
412412
PackageInfo: PackageInfo{
413413
Name: "@livekit/agents",
@@ -467,7 +467,7 @@ func checkPackageLockJSON(filePath, minVersion string) VersionCheckResult {
467467
}
468468

469469
if dep, ok := lockJSON.Dependencies["@livekit/agents"]; ok {
470-
satisfied, err := isVersionSatisfied(dep.Version, minVersion)
470+
satisfied, err := IsVersionSatisfied(dep.Version, minVersion)
471471
return VersionCheckResult{
472472
PackageInfo: PackageInfo{
473473
Name: "@livekit/agents",
@@ -497,7 +497,7 @@ func checkYarnLock(filePath, minVersion string) VersionCheckResult {
497497
matches := pattern.FindStringSubmatch(string(content))
498498
if matches != nil {
499499
version := matches[1]
500-
satisfied, err := isVersionSatisfied(version, minVersion)
500+
satisfied, err := IsVersionSatisfied(version, minVersion)
501501
return VersionCheckResult{
502502
PackageInfo: PackageInfo{
503503
Name: "@livekit/agents",
@@ -527,7 +527,7 @@ func checkPnpmLock(filePath, minVersion string) VersionCheckResult {
527527
matches := pattern.FindStringSubmatch(string(content))
528528
if matches != nil {
529529
version := strings.TrimSpace(matches[1])
530-
satisfied, err := isVersionSatisfied(version, minVersion)
530+
satisfied, err := IsVersionSatisfied(version, minVersion)
531531
return VersionCheckResult{
532532
PackageInfo: PackageInfo{
533533
Name: "@livekit/agents",
@@ -557,7 +557,7 @@ func checkPoetryLock(filePath, minVersion string) VersionCheckResult {
557557
matches := pattern.FindStringSubmatch(string(content))
558558
if matches != nil {
559559
version := matches[1]
560-
satisfied, err := isVersionSatisfied(version, minVersion)
560+
satisfied, err := IsVersionSatisfied(version, minVersion)
561561
return VersionCheckResult{
562562
PackageInfo: PackageInfo{
563563
Name: "livekit-agents",
@@ -587,7 +587,7 @@ func checkUvLock(filePath, minVersion string) VersionCheckResult {
587587
matches := pattern.FindStringSubmatch(string(content))
588588
if matches != nil {
589589
version := matches[1]
590-
satisfied, err := isVersionSatisfied(version, minVersion)
590+
satisfied, err := IsVersionSatisfied(version, minVersion)
591591
return VersionCheckResult{
592592
PackageInfo: PackageInfo{
593593
Name: "livekit-agents",
@@ -617,7 +617,7 @@ func checkPipfileLock(filePath, minVersion string) VersionCheckResult {
617617
matches := pattern.FindStringSubmatch(string(content))
618618
if matches != nil {
619619
version := matches[1]
620-
satisfied, err := isVersionSatisfied(version, minVersion)
620+
satisfied, err := IsVersionSatisfied(version, minVersion)
621621
return VersionCheckResult{
622622
PackageInfo: PackageInfo{
623623
Name: "livekit-agents",
@@ -635,8 +635,8 @@ func checkPipfileLock(filePath, minVersion string) VersionCheckResult {
635635
return VersionCheckResult{}
636636
}
637637

638-
// isVersionSatisfied checks if a version satisfies the minimum requirement
639-
func isVersionSatisfied(version, minVersion string) (bool, error) {
638+
// IsVersionSatisfied checks if a version satisfies the minimum requirement
639+
func IsVersionSatisfied(version, minVersion string) (bool, error) {
640640
// Handle special cases
641641
if version == "latest" || version == "*" || version == "" || version == "next" {
642642
return true, nil // Latest version always satisfies

pkg/agentfs/sdk_version_check_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ func TestIsVersionSatisfied(t *testing.T) {
230230

231231
for _, tt := range tests {
232232
t.Run(tt.version+"_vs_"+tt.minVersion, func(t *testing.T) {
233-
result, err := isVersionSatisfied(tt.version, tt.minVersion)
233+
result, err := IsVersionSatisfied(tt.version, tt.minVersion)
234234

235235
if tt.expectErr {
236236
if err == nil {

0 commit comments

Comments
 (0)