Port simulation text mode handling - #1751
Conversation
…1525) Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Co-authored-by: u9g <jason.lernerman@livekit.io>
Agent.llmNode now returns ReadableStream<ChatChunk | string | FlushSentinel>, but the agent_v2 hook overrides and AgentHookAdapter still declared the narrower ChatChunk | string union, so passing super.llmNode as the fallback failed to type-check. Widen the override return types and the adapter's fallback/return signatures to include FlushSentinel. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brian Yin <brian.yin@livekit.io> Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Co-authored-by: u9g <jason.lernerman@livekit.io>
Catch end-call close listener errors to avoid unhandled rejections during shutdown, and make public tool type guards return false for null inputs.
Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com>
Co-authored-by: rosetta-livekit-bot[bot] <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com>
|
| export function isAgent(obj: unknown): obj is AgentDefinition { | ||
| return typeof obj === 'object' && obj !== null && AGENT_DEFINITION_SYMBOL in obj; |
There was a problem hiding this comment.
🚩 isAgent detection changed from structural to symbol-based — existing non-defineAgent exports will break
The isAgent function at agents/src/generator.ts:20-21 now checks for AGENT_DEFINITION_SYMBOL instead of doing a structural duck-type check (has entry function, optionally has prewarm function). This means any module that exports a plain { entry, prewarm } object without going through defineAgent() will no longer be recognized as an agent by the framework's worker/IPC layer. The new test at agents/src/generator.test.ts:17 explicitly verifies this: isAgent({ entry: async () => {} }) returns false. This is an intentional breaking change, but it could silently break users who export agent definitions without defineAgent(). The changeset doesn't call this out as a breaking change.
Was this helpful? React with 👍 or 👎 to provide feedback.
| updateTools: (tools) => { | ||
| ts._setTools(tools); | ||
| void this.onToolsetToolsChanged().catch((error) => | ||
| this.logger.error({ error }, 'error re-advertising toolset tools'), | ||
| ); |
There was a problem hiding this comment.
🚩 onToolsetToolsChanged is fire-and-forget — concurrent pushes could race
At agents/src/voice/agent_activity.ts:4229-4233, when a toolset pushes new tools via updateTools, the handler calls this.onToolsetToolsChanged() as a fire-and-forget promise (void ... .catch(...)). If a toolset pushes tools rapidly (e.g., an MCP server reconnecting), multiple onToolsetToolsChanged calls could overlap. Each call invokes this.updateTools(current.tools) which modifies this.agent._toolCtx, sets up/closes toolsets, and updates the realtime session. The updateTools method itself is not guarded by a mutex, so concurrent invocations could interleave their reads of oldToolCtx and writes to agent._toolCtx. In practice, dynamic tool pushes are likely infrequent enough that this doesn't manifest, but it's worth noting for robustness.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
start --simulationflag to disable worker load-limit status changes during simulationsTesting
pnpm exec prettier --check "agents/src/{cli,job,worker}.ts" "agents/src/voice/{agent_activity,agent_session}.ts"pnpm --filter @livekit/agents buildpnpm --filter @livekit/agents lint(warnings only, pre-existing)pnpm --filter @livekit/agents api:check(fails on existing API Extractor limitation:export * as ___indist/index.d.ts)Ported from livekit/agents#6036
Original PR description
What
Worker load limit off under simulation.
lk simulatenow launches the agent with a new--simulationflag (supported on bothpython -m livekit.agents startand the legacy typerstart; hidden from help in the latter). It sets simulation mode onAgentServer:_is_available()always accepts (draining still wins) and_update_worker_status()never reportsWS_FULL, so a run can saturate the agent instead of being throttled byload_threshold. Deliberately not implemented by forcingload_threshold=inf, whichServerOptionsvalidation rejects outside dev mode (and Cloud reverts custom thresholds anyway).Text simulations skip STT/TTS. The simulate worker now stamps
SimulationModeintoSimulationDispatch(agents-private#68).SimulationContext.channelexposes it (unspecified ⇒ TEXT, since older dispatches were all text-only), andAgentSession.start()drops STT/TTS/VAD and disables RoomIO audio I/O when running under a TEXT-mode simulation — they're never initialized or connected, even though the user's code passes them.Forward-compat:
simulation_context()now parses the dispatch withignore_unknown_fields=True, so future dispatch fields don't break older agents.Tests
tests/test_simulation_text_channel.py:_is_available()under full load: rejected normally, accepted in simulation mode, draining still rejectssession.start()under a mocked TEXT-sim job context ends withstt/tts/vad = NoneVerified the protojson produced by the Go worker handler round-trips into
SimulationContext.channel == TEXT, including with unknown future fields injected.Dependencies (draft until released)
Requires a livekit-protocol release containing
SimulationMode(livekit/protocol#1613 via livekit/python-sdks#710) —livekit.agents.__init__importssimulationeagerly, so the package needs the new bindings; bump thelivekit-protocolfloor in pyproject when it ships.Related: livekit/agents-private#68, livekit/cloud-api-server#1892, lk CLI PR (livekit-cli).