Skip to content

Tighten agent session e2e shutdown assertions#883

Merged
u9g merged 2 commits into
feat/agent-session-daemonfrom
jason/session-e2e-shutdown-assertions
Jun 23, 2026
Merged

Tighten agent session e2e shutdown assertions#883
u9g merged 2 commits into
feat/agent-session-daemonfrom
jason/session-e2e-shutdown-assertions

Conversation

@u9g

@u9g u9g commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • use a shared 5s timeout for session e2e lifecycle operations
  • assert post-shutdown say fails with exit code 1 and no matched token
  • verify the session port is free before and after the failed say

Test

  • set -a; source .env; set +a; go test ./cmd/lk -run TestSessionE2E -count=1 -v

The detached daemon boot + agent venv + LLM connect takes longer than
the shared 5s sessionE2ETimeout, which killed start on the ubuntu and
windows CI runners (signal: killed / exit status 1). Bump start to 15s.
@u9g u9g merged commit 1c2c4ba into feat/agent-session-daemon Jun 23, 2026
5 checks passed
@u9g u9g deleted the jason/session-e2e-shutdown-assertions branch June 23, 2026 19:12
toubatbrian added a commit that referenced this pull request Jun 24, 2026
…857)

* feat(cli): add `lk agent session` for headless text-mode agent runs

Introduces a three-process model (ephemeral CLI command, detached
singleton daemon, agent subprocess) that drives a Python/JS agent over
TCP using the lk.agent.session protobuf protocol, with no audio/CGO
dependency:

- `lk agent session start <file>`: re-execs the lk binary as a detached
  daemon bound to a fixed loopback port (singleton), which spawns the
  agent and applies text mode; rejects start if a session already runs.
- `lk agent session say "..."`: streams a user turn and renders the
  agent reply, tool calls/outputs, and handoffs to the terminal.
- `lk agent session end`: tears down the daemon and agent.

The CLI<->daemon control protocol reuses pkg/ipc length-prefixed framing
over the same TCP port, disambiguated from agent connections by a magic
preamble. The headless renderer covers all ChatItem variants plus the
FunctionToolsExecuted event. Drops the now-unnecessary U1000 file-ignore
directives added while the helpers were unused.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): skip empty function-tool output line in session renderer

Tools that return no string (e.g. handoff tools returning an Agent)
produced a bare "✓ " line. Suppress the output line when the summarized
output is empty for successful calls; error outputs still render.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(cli): dispatch session daemon via hidden subcommand entrypoint

Replace the env-gated branch at the top of main() with a dedicated,
hidden `lk agent session daemon` subcommand (mirroring the existing
hidden `generate-fish-completion` command). `start` now re-execs the
binary into that subcommand instead of setting LK_SESSION_DAEMON=1, so
the daemon has its own entrypoint dispatched by the CLI framework rather
than special-casing main(). Re-exec of the same binary is retained
(a separate binary can't be located reliably after `go install`);
runtime params still flow through the LK_SESSION_* env vars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): reject direct invocation of hidden session daemon entrypoint

A registered subcommand is always invokable (Hidden only drops it from
help), so a stray `lk agent session daemon` previously spawned a
half-configured daemon (random port, empty project dir) that exited
silently. Guard the entrypoint on the inherited readiness pipe that
`start` always provides: without it, return a clear error directing the
user to `lk agent session start`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update session.go

* test(cli): e2e test for the agent session lifecycle (#882)

* test(cli): add e2e test for the agent session lifecycle

Adds an opt-in end-to-end test that drives the real `lk agent session`
start/say/end flow against a minimal one-file echo agent (testdata/echo-agent),
asserting the model echoes a token back and that the detached daemon exits
afterward. The fixture is a uv project so the daemon's `uv run python`
auto-syncs deps; its __main__ dispatches console mode to the TCP console
directly since cli.run_app() doesn't expose --connect-addr on released agents.

Includes a GitHub Actions workflow that runs the test on Linux and Windows,
triggered by workflow_dispatch and pushes to any repo branch. Gated behind
LIVEKIT_API_KEY so it skips without credentials.

* fix(cli): use a readiness file instead of an inherited pipe fd

The session daemon spawn passed the readiness pipe to the detached child via
cmd.ExtraFiles (fd 3), but os/exec's ExtraFiles is unsupported on Windows, so
daemon.Start() failed with "fork/exec ...: not supported by windows" and the
session never started there.

Replace the inherited fd with a temp readiness file: the daemon writes its
status atomically (write + rename) and `start` polls it until it sees a status,
the daemon process exits, or a timeout slightly past the daemon's own
agent-connect deadline. Works identically on Linux and Windows.

* chore: regenerate fish_autocomplete for agent session command

* ci(session-e2e): build the Windows arm via Linux cross-compile

Round out the Session E2E workflow: add the macOS arm, wire up the
portaudio submodule + ALSA headers, and build Windows with zig to match
.goreleaser.yaml's toolchain. Native Windows can't link the
webrtc/portaudio cgo objects (the ~560-object link overflows the
command-line limit), so cross-compile lk.exe and the e2e test binary on
Linux and run them natively on Windows. buildLK honors LK_SESSION_E2E_BIN
so the Windows runner drives the prebuilt binary instead of rebuilding.

* Tighten agent session e2e shutdown assertions (#883)

* Tighten agent session e2e shutdown assertions

* test(session-e2e): give start a 15s timeout

The detached daemon boot + agent venv + LLM connect takes longer than
the shared 5s sessionE2ETimeout, which killed start on the ubuntu and
windows CI runners (signal: killed / exit status 1). Bump start to 15s.

* feat(cli): rename "lk agent session end" to "stop"

Rename the `end` subcommand of `lk agent session` to `stop` per review
feedback. Renames the CLI subcommand, the runSessionEnd handler to
runSessionStop, the control-protocol verb ("end" -> "stop") on both the
client and daemon sides, usage strings, comments, the e2e test, and the
generated fish completion.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli): rename "lk agent session" command to "daemon"

Address review feedback (theomonnom): the user-facing top-level command
is now `lk agent daemon {start,say,stop}`. The hidden internal re-exec
entrypoint that `start` launches is renamed `daemon` -> `run` to avoid the
awkward `daemon daemon` path, so `start` now re-execs `lk agent daemon run`.
Updates all user-facing strings, the e2e test invocations, the session-e2e
workflow header, and regenerates fish completions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli): rename hidden daemon entrypoint to "serve"

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(session-e2e): simplify echo-agent entrypoint to cli.run_app

The daemon launches the agent via the thin CLI
(python -m livekit.agents console <entrypoint> --connect-addr), which
discovers the server and dispatches to the TCP console itself. The
agent file's __main__ console-dispatch hack was compensating for the
old python agent.py launch path and is now dead code. Collapse it to
the thin cli.run_app(server) form, matching agent-starter-python.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: u9g <jason.lernerman@livekit.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants