From 0b8c65d88fafbe9c746767a57b8b3a2b2dc45117 Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 06:34:32 +0000 Subject: [PATCH 1/2] feat(agents): read simulator dispatch metadata --- agents/src/constants.ts | 3 +- agents/src/job.ts | 88 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/agents/src/constants.ts b/agents/src/constants.ts index 3da61af46..da8acb173 100644 --- a/agents/src/constants.ts +++ b/agents/src/constants.ts @@ -11,7 +11,8 @@ export const TOPIC_CHAT = 'lk.chat'; export const ATTRIBUTE_AGENT_STATE = 'lk.agent.state'; export const ATTRIBUTE_AGENT_NAME = 'lk.agent.name'; -// TODO(eval): export const ATTRIBUTE_SIMULATOR = 'lk.simulator'; +export const ATTRIBUTE_SIMULATOR = 'lk.simulator'; +export const ATTRIBUTE_SIMULATOR_DISPATCH = 'lk.simulator.dispatch'; export const TOPIC_CLIENT_EVENTS = 'lk.agent.events'; export const RPC_GET_SESSION_STATE = 'lk.agent.get_session_state'; diff --git a/agents/src/job.ts b/agents/src/job.ts index efbd02fb6..471e934b4 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -17,6 +17,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import * as os from 'node:os'; import * as path from 'node:path'; import type { Logger } from 'pino'; +import { ATTRIBUTE_SIMULATOR, ATTRIBUTE_SIMULATOR_DISPATCH } from './constants.js'; import type { InferenceExecutor } from './ipc/inference_executor.js'; import { log } from './log.js'; import { flushOtelLogs, setupCloudTracer, uploadSessionReport } from './telemetry/index.js'; @@ -90,6 +91,49 @@ export type RunningJobInfo = { apiSecret?: string; }; +export enum SimulationMode { + SIMULATION_MODE_UNSPECIFIED = 0, + SIMULATION_MODE_TEXT = 1, + SIMULATION_MODE_AUDIO = 2, +} + +export type SimulationDispatch = { + simulationRunId?: string; + simulation_run_id?: string; + mode?: string | number; + scenario?: unknown; +}; + +export class SimulationContext> { + #dispatch: SimulationDispatch; + #jobContext: JobContext; + + constructor(dispatch: SimulationDispatch, jobContext: JobContext) { + this.#dispatch = dispatch; + this.#jobContext = jobContext; + } + + get scenario(): unknown { + return this.#dispatch.scenario; + } + + get simulationMode(): SimulationMode { + const mode = this.#dispatch.mode; + if (mode === SimulationMode.SIMULATION_MODE_AUDIO || mode === 'SIMULATION_MODE_AUDIO') { + return SimulationMode.SIMULATION_MODE_AUDIO; + } + if (mode === SimulationMode.SIMULATION_MODE_TEXT || mode === 'SIMULATION_MODE_TEXT') { + return SimulationMode.SIMULATION_MODE_TEXT; + } + // Simulations predating the mode field were text-only. + return SimulationMode.SIMULATION_MODE_TEXT; + } + + get jobContext(): JobContext { + return this.#jobContext; + } +} + /** Attempted to add a function callback, but the function already exists. */ export class FunctionExistsError extends Error { constructor(msg?: string) { @@ -119,6 +163,8 @@ export class JobContext> { } = {}; #logger: Logger; #inferenceExecutor: InferenceExecutor; + #simulationResolved = false; + #simulationContext?: SimulationContext; /** @internal */ _primaryAgentSession?: AgentSession; @@ -172,6 +218,48 @@ export class JobContext> { return this.#info; } + simulationContext(): SimulationContext | undefined { + if (this.#simulationResolved) { + return this.#simulationContext; + } + + this.#simulationResolved = true; + + let metadata = ''; + for (const participant of this.#room.remoteParticipants.values()) { + if (!Object.hasOwn(participant.attributes, ATTRIBUTE_SIMULATOR)) { + continue; + } + + metadata = participant.attributes[ATTRIBUTE_SIMULATOR_DISPATCH] || ''; + if (metadata) { + break; + } + } + + if (!metadata) { + // Older servers and fake job contexts placed the dispatch in job metadata. + metadata = (this.#info.job as proto.Job & { metadata?: string }).metadata || ''; + } + if (!metadata) { + return undefined; + } + + let dispatch: SimulationDispatch; + try { + dispatch = JSON.parse(metadata) as SimulationDispatch; + } catch { + return undefined; + } + + if (!(dispatch.simulationRunId || dispatch.simulation_run_id)) { + return undefined; + } + + this.#simulationContext = new SimulationContext(dispatch, this); + return this.#simulationContext; + } + /** @returns The agent's participant if connected to the room, otherwise `undefined` */ get agent(): LocalParticipant | undefined { return this.#room.localParticipant; From 98ba6a8c32226e265a9e00e133238f7a18577b59 Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:29:49 +0000 Subject: [PATCH 2/2] simulation: don't cache pre-connect context miss --- agents/src/job.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agents/src/job.ts b/agents/src/job.ts index 471e934b4..cac316859 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -223,8 +223,6 @@ export class JobContext> { return this.#simulationContext; } - this.#simulationResolved = true; - let metadata = ''; for (const participant of this.#room.remoteParticipants.values()) { if (!Object.hasOwn(participant.attributes, ATTRIBUTE_SIMULATOR)) { @@ -242,9 +240,14 @@ export class JobContext> { metadata = (this.#info.job as proto.Job & { metadata?: string }).metadata || ''; } if (!metadata) { + // The simulator participant is only visible once the room is connected; a pre-connect miss + // must not prevent a later lookup from finding the simulator dispatch. + this.#simulationResolved = this.#room.isConnected; return undefined; } + this.#simulationResolved = true; + let dispatch: SimulationDispatch; try { dispatch = JSON.parse(metadata) as SimulationDispatch;