Skip to content

Commit f9b2e56

Browse files
committed
agent CLI: friendly errors for old SDK + worker start failures
- Pre-flight: reuse agentfs.CheckSDKVersion (pinned to 1.6.0, the thin-CLI baseline) in startAgent so start/dev/console/simulate fail fast with 'too old, upgrade to ...' instead of a cryptic subprocess error. - diagnoseAgentFailure: when the worker exits early or never registers/connects, infer the likely cause (missing venv/deps vs bad LIVEKIT_ credentials), show the agent's output tail, and point at the full log. Wired into simulate (CI + TUI) and console.
1 parent 1a500d8 commit f9b2e56

4 files changed

Lines changed: 59 additions & 27 deletions

File tree

cmd/lk/console.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"io"
2121
"log"
2222
"net"
23-
"os"
2423
"os/signal"
2524
"strings"
2625
"syscall"
@@ -171,23 +170,12 @@ func runConsole(ctx context.Context, cmd *cli.Command) error {
171170
return fmt.Errorf("agent connection: %w", res.err)
172171
}
173172
conn = res.conn
174-
case err := <-agentProc.Done():
173+
case <-agentProc.Done():
175174
stopSpinner()
176-
logs := agentProc.RecentLogs(20)
177-
for _, l := range logs {
178-
fmt.Fprintln(os.Stderr, l)
179-
}
180-
if err != nil {
181-
return fmt.Errorf("agent exited before connecting: %w", err)
182-
}
183-
return fmt.Errorf("agent exited before connecting")
175+
return fmt.Errorf("the agent exited before connecting.\n\n%s", diagnoseAgentFailure(agentProc))
184176
case <-time.After(60 * time.Second):
185177
stopSpinner()
186-
logs := agentProc.RecentLogs(20)
187-
for _, l := range logs {
188-
fmt.Fprintln(os.Stderr, l)
189-
}
190-
return fmt.Errorf("timed out waiting for agent to connect")
178+
return fmt.Errorf("timed out waiting for the agent to connect.\n\n%s", diagnoseAgentFailure(agentProc))
191179
case <-ctx.Done():
192180
stopSpinner()
193181
return ctx.Err()

cmd/lk/simulate_ci.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,12 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error {
8484
case <-agent.Ready():
8585
logFwd.enabled.Store(false)
8686
fmt.Fprintf(os.Stdout, "✓ Agent registered (%s)\n", time.Since(start).Round(time.Millisecond))
87-
case err := <-agent.Done():
87+
case <-agent.Done():
8888
fmt.Fprintln(os.Stdout, "::endgroup::")
89-
if err != nil {
90-
return fmt.Errorf("agent exited before registering: %w", err)
91-
}
92-
return fmt.Errorf("agent exited before registering")
89+
return fmt.Errorf("the agent exited before registering.\n\n%s", diagnoseAgentFailure(agent))
9390
case <-timeout.C:
9491
fmt.Fprintln(os.Stdout, "::endgroup::")
95-
return fmt.Errorf("timed out waiting for agent to register (%s)", agentRegisterTimeout)
92+
return fmt.Errorf("timed out after %s waiting for the agent to register.\n\n%s", agentRegisterTimeout, diagnoseAgentFailure(agent))
9693
case <-ctx.Done():
9794
fmt.Fprintln(os.Stdout, "::endgroup::")
9895
return ctx.Err()

cmd/lk/simulate_subprocess.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,63 @@ type AgentStartConfig struct {
134134
ForwardOutput io.Writer // if set, forward each output line to this writer
135135
}
136136

137+
// thinCLIMinVersion is the first livekit-agents release that exposes the
138+
// start/dev/console/simulate subcommands under `python -m livekit.agents`.
139+
const thinCLIMinVersion = "1.6.0"
140+
141+
// diagnoseAgentFailure turns an agent that exited early or never registered into
142+
// a human-friendly explanation, inferring common causes from its output.
143+
func diagnoseAgentFailure(ap *AgentProcess) string {
144+
logs := ap.RecentLogs(0)
145+
hay := strings.ToLower(strings.Join(logs, "\n"))
146+
var hint string
147+
switch {
148+
case strings.Contains(hay, "modulenotfounderror"), strings.Contains(hay, "no module named"):
149+
hint = "The Python environment is missing dependencies - check that the right virtualenv is active and the agent is installed (pip install / uv sync)."
150+
case strings.Contains(hay, "api key"), strings.Contains(hay, "api_key"),
151+
strings.Contains(hay, "api secret"), strings.Contains(hay, "unauthorized"),
152+
strings.Contains(hay, "invalid token"), strings.Contains(hay, "401"):
153+
hint = "Looks like a credentials problem - check LIVEKIT_URL, LIVEKIT_API_KEY and LIVEKIT_API_SECRET."
154+
default:
155+
hint = "The agent worker didn't start. Its output below (and the full log) should show why."
156+
}
157+
msg := hint
158+
if tail := lastNonEmptyLines(logs, 8); len(tail) > 0 {
159+
msg += "\n\nAgent output:\n " + strings.Join(tail, "\n ")
160+
}
161+
if ap.LogPath != "" {
162+
msg += "\n\nFull agent log: " + ap.LogPath
163+
}
164+
return msg
165+
}
166+
167+
// lastNonEmptyLines returns up to n trailing non-blank lines, in order.
168+
func lastNonEmptyLines(lines []string, n int) []string {
169+
var out []string
170+
for i := len(lines) - 1; i >= 0 && len(out) < n; i-- {
171+
if strings.TrimSpace(lines[i]) != "" {
172+
out = append([]string{lines[i]}, out...)
173+
}
174+
}
175+
return out
176+
}
177+
137178
// startAgent launches a Python agent subprocess and monitors its output.
138179
func startAgent(cfg AgentStartConfig) (*AgentProcess, error) {
139180
pythonBin, prefixArgs, err := findPythonBinary(cfg.Dir, cfg.ProjectType)
140181
if err != nil {
141182
return nil, err
142183
}
143184

185+
// Reuse the SDK-version reader (parses the project's deps) to fail fast with a
186+
// friendly message when livekit-agents is older than the thin-CLI baseline.
187+
if err := agentfs.CheckSDKVersion(cfg.Dir, cfg.ProjectType, map[string]string{
188+
"python-min-sdk-version": thinCLIMinVersion,
189+
"node-min-sdk-version": thinCLIMinVersion,
190+
}); err != nil {
191+
return nil, err
192+
}
193+
144194
// Launch via the framework CLI module rather than running the user's file
145195
// directly: python -m livekit.agents SUBCOMMAND ENTRYPOINT FLAGS. The framework
146196
// discovers the AgentServer from the entrypoint and drives the thin CLI. Requires a

cmd/lk/simulate_tui.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,14 +296,11 @@ func (m *simulateModel) waitAgentReadyCmd() tea.Cmd {
296296
select {
297297
case <-m.agent.Ready():
298298
return agentReadyMsg{elapsed: time.Since(stepStart)}
299-
case err := <-m.agent.Done():
300-
if err != nil {
301-
return agentReadyMsg{err: fmt.Errorf("agent exited before registering: %w", err)}
302-
}
303-
return agentReadyMsg{err: fmt.Errorf("agent exited before registering")}
299+
case <-m.agent.Done():
300+
return agentReadyMsg{err: fmt.Errorf("the agent exited before registering.\n\n%s", diagnoseAgentFailure(m.agent))}
304301
case <-timeout.C:
305302
m.agent.Kill()
306-
return agentReadyMsg{err: fmt.Errorf("timed out waiting for agent to register (%s)", agentRegisterTimeout)}
303+
return agentReadyMsg{err: fmt.Errorf("timed out after %s waiting for the agent to register.\n\n%s", agentRegisterTimeout, diagnoseAgentFailure(m.agent))}
307304
case <-m.setupCtx.Done():
308305
return agentReadyMsg{err: m.setupCtx.Err()}
309306
}

0 commit comments

Comments
 (0)