Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agents/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
91 changes: 91 additions & 0 deletions agents/src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ProcessUserData = Record<string, unknown>> {
#dispatch: SimulationDispatch;
#jobContext: JobContext<ProcessUserData>;

constructor(dispatch: SimulationDispatch, jobContext: JobContext<ProcessUserData>) {
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<ProcessUserData> {
return this.#jobContext;
}
}

/** Attempted to add a function callback, but the function already exists. */
export class FunctionExistsError extends Error {
constructor(msg?: string) {
Expand Down Expand Up @@ -119,6 +163,8 @@ export class JobContext<ProcessUserData = Record<string, unknown>> {
} = {};
#logger: Logger;
#inferenceExecutor: InferenceExecutor;
#simulationResolved = false;
#simulationContext?: SimulationContext<ProcessUserData>;

/** @internal */
_primaryAgentSession?: AgentSession;
Expand Down Expand Up @@ -172,6 +218,51 @@ export class JobContext<ProcessUserData = Record<string, unknown>> {
return this.#info;
}

simulationContext(): SimulationContext<ProcessUserData> | undefined {
if (this.#simulationResolved) {
return this.#simulationContext;
}

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) {
// 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;
} 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;
Expand Down