Skip to content

Commit 8df0f96

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 a clear 'too old, upgrade to ...' instead of a cryptic subprocess error. - On early exit / failed register/connect, surface the agent's OWN output (the real error) plus the full log path - no string-match guessing of the cause. Wired into simulate (CI + TUI) and console. Verified against staging with a broken .venv (shows the ModuleNotFoundError) and an old declared SDK (shows the upgrade message).
1 parent 1a500d8 commit 8df0f96

4 files changed

Lines changed: 50 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", agentExitDetail(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", agentExitDetail(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", agentExitDetail(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, agentExitDetail(agent))
9693
case <-ctx.Done():
9794
fmt.Fprintln(os.Stdout, "::endgroup::")
9895
return ctx.Err()

cmd/lk/simulate_subprocess.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,54 @@ 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+
// agentExitDetail surfaces the agent's own output (the real error) and a pointer
142+
// to the full log, for when the worker exits early or never registers/connects.
143+
// It does not try to guess the cause — the agent's output already says it.
144+
func agentExitDetail(ap *AgentProcess) string {
145+
var b strings.Builder
146+
if tail := lastNonEmptyLines(ap.RecentLogs(0), 12); len(tail) > 0 {
147+
b.WriteString("Agent output:\n " + strings.Join(tail, "\n "))
148+
}
149+
if ap.LogPath != "" {
150+
if b.Len() > 0 {
151+
b.WriteString("\n\n")
152+
}
153+
b.WriteString("Full log: " + ap.LogPath)
154+
}
155+
return b.String()
156+
}
157+
158+
// lastNonEmptyLines returns up to n trailing non-blank lines, in order.
159+
func lastNonEmptyLines(lines []string, n int) []string {
160+
var out []string
161+
for i := len(lines) - 1; i >= 0 && len(out) < n; i-- {
162+
if strings.TrimSpace(lines[i]) != "" {
163+
out = append([]string{lines[i]}, out...)
164+
}
165+
}
166+
return out
167+
}
168+
137169
// startAgent launches a Python agent subprocess and monitors its output.
138170
func startAgent(cfg AgentStartConfig) (*AgentProcess, error) {
139171
pythonBin, prefixArgs, err := findPythonBinary(cfg.Dir, cfg.ProjectType)
140172
if err != nil {
141173
return nil, err
142174
}
143175

176+
// Reuse the SDK-version reader (parses the project's deps) to fail fast with a
177+
// friendly message when livekit-agents is older than the thin-CLI baseline.
178+
if err := agentfs.CheckSDKVersion(cfg.Dir, cfg.ProjectType, map[string]string{
179+
"python-min-sdk-version": thinCLIMinVersion,
180+
"node-min-sdk-version": thinCLIMinVersion,
181+
}); err != nil {
182+
return nil, err
183+
}
184+
144185
// Launch via the framework CLI module rather than running the user's file
145186
// directly: python -m livekit.agents SUBCOMMAND ENTRYPOINT FLAGS. The framework
146187
// 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", agentExitDetail(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, agentExitDetail(m.agent))}
307304
case <-m.setupCtx.Done():
308305
return agentReadyMsg{err: m.setupCtx.Err()}
309306
}

0 commit comments

Comments
 (0)