diff --git a/.github/workflows/session-e2e.yaml b/.github/workflows/session-e2e.yaml index 7b818b14f..d6222628d 100644 --- a/.github/workflows/session-e2e.yaml +++ b/.github/workflows/session-e2e.yaml @@ -1,23 +1,27 @@ name: Session E2E # Drives the real `lk agent daemon` start/say/stop lifecycle against the minimal -# one-file echo agent in cmd/lk/testdata/echo-agent, on Linux, macOS, and -# Windows. This exercises the detached daemon, the readiness handshake, the -# console IPC transport, and the model round-trip end to end -- runtime behavior -# that `go test` alone never covers. +# one-file echo agents in cmd/lk/testdata, on Linux, macOS, and Windows. This +# exercises the detached daemon, the readiness handshake, the console IPC +# transport, and the model round-trip end to end -- runtime behavior that +# `go test` alone never covers. +# +# Both agent runtimes are exercised via the `lang` matrix dimension: Python +# (testdata/echo-agent, the test's default) and Node (testdata/echo-agent-node, +# selected with LK_SESSION_E2E_AGENT). The daemon launches each as +# ` -m livekit.agents console ...` or `node console ...`, so a +# pass proves the language-specific launch path as well as the shared transport. # # Runs on manual dispatch and on pushes to any repo branch (forks can't trigger # `push`, so secrets are only exposed to trusted collaborators). It needs live # LiveKit credentials -- set these repo secrets first: LIVEKIT_API_KEY, -# LIVEKIT_API_SECRET, LIVEKIT_URL. The echo agent drives its LLM through LiveKit -# Inference, so no other provider keys are needed. +# LIVEKIT_API_SECRET, LIVEKIT_URL. The echo agents drive their LLM through +# LiveKit Inference, so no other provider keys are needed. # -# The echo agent depends on plain PyPI livekit-agents (synced by `uv sync` from -# its pyproject.toml). Note: on current releases/main, `cli.run_app()` routes -# `console` through the legacy click CLI, which has no --connect-addr (that -# lives behind `python -m livekit.agents`). The fixture's __main__ dispatches -# console mode to the TCP console directly to bridge the daemon's -# `python console --connect-addr` launch. +# Each agent's deps are resolved fresh in CI (they're gitignored): the Python +# fixture via `uv sync` from its pyproject.toml; the Node fixture via +# `npm install` from its package.json. The Node `.ts` entrypoint runs through +# Node's --experimental-strip-types loader, which needs Node >= 22.6. # # Windows is split into two jobs: pkg/apm/webrtc's C++ uses MSVC SEH # (__try/__except) that the runner's mingw GCC can't compile, so we build with @@ -26,12 +30,8 @@ name: Session E2E # ~32KB command-line limit. So `cross-build-windows` cross-compiles lk.exe AND # the e2e test binary on Linux and uploads them; `e2e-windows` downloads them # and runs the test natively (LK_SESSION_E2E_BIN points the test at the -# prebuilt lk, so nothing is rebuilt on the Windows runner). -# -# Node is intentionally not in the matrix yet: this branch's session daemon only -# supports Python agents (`detectProject` rejects non-Python), and Node console -# support depends on the brian/agent-session-node-support CLI line (#868/#878) -# plus agents-js #1804. Add a node arm once those land. +# prebuilt lk, so nothing is rebuilt on the Windows runner). The binaries are +# language-agnostic, so that build runs once and feeds every `lang` arm. on: workflow_dispatch: @@ -50,9 +50,10 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] + lang: [python, node] runs-on: ${{ matrix.os }} - name: Agent Session with Python Agent on ${{ matrix.os }} + name: lk agent with ${{ matrix.lang }} agent on ${{ matrix.os }} permissions: contents: read @@ -76,24 +77,42 @@ jobs: cache: true - name: Set up Python + if: matrix.lang == 'python' uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up uv + if: matrix.lang == 'python' uses: astral-sh/setup-uv@v5 with: enable-cache: true - name: Sync echo agent deps + if: matrix.lang == 'python' working-directory: cmd/lk/testdata/echo-agent run: uv sync + - name: Set up Node + if: matrix.lang == 'node' + uses: actions/setup-node@v4 + with: + # --experimental-strip-types (used to run the .ts entrypoint) needs >= 22.6. + node-version: "22" + + - name: Install Node echo agent deps + if: matrix.lang == 'node' + working-directory: cmd/lk/testdata/echo-agent-node + run: npm install + - name: Run session e2e env: LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }} LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }} LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }} + # Default (unset) targets the Python fixture; point at the Node one for that arm. + # Resolved relative to cmd/lk, the test binary's working directory. + LK_SESSION_E2E_AGENT: ${{ matrix.lang == 'node' && 'testdata/echo-agent-node/agent.ts' || '' }} run: go test ./cmd/lk -run TestSessionE2E -count=1 -v -timeout 600s # Cross-compile the Windows artifacts on Linux with zig, mirroring @@ -159,7 +178,12 @@ jobs: e2e-windows: needs: cross-build-windows runs-on: windows-latest - name: Agent Session with Python Agent on windows-latest + name: Agent Session with ${{ matrix.lang }} Agent on windows-latest + + strategy: + fail-fast: false + matrix: + lang: [python, node] permissions: contents: read @@ -168,7 +192,7 @@ jobs: - name: Checkout livekit-cli uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # No submodules: the prebuilt binaries already contain the cgo code; - # only the echo-agent fixture (plain repo files) is needed at runtime. + # only the echo-agent fixtures (plain repo files) are needed at runtime. - name: Download Windows artifacts uses: actions/download-artifact@v4 @@ -177,22 +201,37 @@ jobs: path: dist - name: Set up Python + if: matrix.lang == 'python' uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up uv + if: matrix.lang == 'python' uses: astral-sh/setup-uv@v5 with: enable-cache: true - name: Sync echo agent deps + if: matrix.lang == 'python' working-directory: cmd/lk/testdata/echo-agent run: uv sync + - name: Set up Node + if: matrix.lang == 'node' + uses: actions/setup-node@v4 + with: + # --experimental-strip-types (used to run the .ts entrypoint) needs >= 22.6. + node-version: "22" + + - name: Install Node echo agent deps + if: matrix.lang == 'node' + working-directory: cmd/lk/testdata/echo-agent-node + run: npm install + - name: Run session e2e - # Run from cmd/lk so the test's default testdata/echo-agent/agent.py - # path resolves; point it at the cross-built lk.exe so nothing rebuilds. + # Run from cmd/lk so the test's relative testdata/ entrypoint paths + # resolve; point it at the cross-built lk.exe so nothing rebuilds. shell: bash working-directory: cmd/lk env: @@ -202,6 +241,8 @@ jobs: LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }} LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }} LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }} + # Default (unset) targets the Python fixture; point at the Node one for that arm. + LK_SESSION_E2E_AGENT: ${{ matrix.lang == 'node' && 'testdata/echo-agent-node/agent.ts' || '' }} run: | ../../dist/session-e2e.test.exe \ -test.run TestSessionE2E -test.count=1 -test.v -test.timeout 600s diff --git a/.gitignore b/.gitignore index 13af04473..b18e3fff5 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,7 @@ cmd/lk/testdata/**/.venv # uv lockfiles for test agent fixtures (resolved fresh in CI) cmd/lk/testdata/**/uv.lock + +# node deps installed for e2e agent fixtures (resolved fresh in CI) +cmd/lk/testdata/**/node_modules +cmd/lk/testdata/**/package-lock.json diff --git a/cmd/lk/testdata/echo-agent-node/agent.ts b/cmd/lk/testdata/echo-agent-node/agent.ts new file mode 100644 index 000000000..faafe83d6 --- /dev/null +++ b/cmd/lk/testdata/echo-agent-node/agent.ts @@ -0,0 +1,30 @@ +/** + * Minimal one-file echo agent for the `lk agent daemon` e2e test -- the Node + * sibling of testdata/echo-agent/agent.py. + * + * Driven in text mode, so an LLM is the only component needed. Echoes the + * user's text verbatim, which the test asserts on. + */ +import { type JobContext, ServerOptions, cli, defineAgent, inference, voice } from '@livekit/agents'; +import 'dotenv/config'; +import { fileURLToPath } from 'node:url'; + +export default defineAgent({ + entry: async (ctx: JobContext) => { + const session = new voice.AgentSession({ + llm: new inference.LLM({ model: 'openai/gpt-4o-mini' }), + }); + await session.start({ + agent: new voice.Agent({ + instructions: + 'You are an echo bot. Reply with exactly the text the user sends, verbatim, and nothing else.', + }), + room: ctx.room, + // No TTS, so disable audio output or the turn crashes in the tts node. + outputOptions: { audioEnabled: false }, + }); + await ctx.connect(); + }, +}); + +cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) })); diff --git a/cmd/lk/testdata/echo-agent-node/package.json b/cmd/lk/testdata/echo-agent-node/package.json new file mode 100644 index 000000000..d6770dd33 --- /dev/null +++ b/cmd/lk/testdata/echo-agent-node/package.json @@ -0,0 +1,12 @@ +{ + "name": "lk-session-e2e-echo-agent-node", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@livekit/agents": "^1.4.9", + "@livekit/rtc-node": "^0.13.29", + "dotenv": "^16.4.5", + "zod": "^3.25.76" + } +}