From 0c2db03809a892efd8f16a9096efb7f318377f76 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Tue, 18 Aug 2026 15:25:13 -0400 Subject: [PATCH 1/2] simulate: pass the simulation mode into runSimulate --- cmd/lk/simulate.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index 0588e4c1..371d1ae1 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -70,7 +70,13 @@ var simulateCommand = &cli.Command{ simulateProjectConfig = pc return nil, nil }, - Action: runSimulate, + Action: func(ctx context.Context, cmd *cli.Command) error { + mode := livekit.SimulationMode_SIMULATION_MODE_TEXT + if cmd.Bool("audio") { + mode = livekit.SimulationMode_SIMULATION_MODE_AUDIO + } + return runSimulate(ctx, cmd, mode) + }, Flags: []cli.Flag{ &cli.IntFlag{ Name: "num-simulations", @@ -273,7 +279,7 @@ func buildTaskExists(projectDir string) (bool, error) { return ok, nil } -func runSimulate(ctx context.Context, cmd *cli.Command) error { +func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.SimulationMode) error { pc := simulateProjectConfig // --export is a one-shot read of a finished run, so it short-circuits @@ -368,11 +374,6 @@ func runSimulate(ctx context.Context, cmd *cli.Command) error { simClient := lksdk.NewAgentSimulationClient(serverURL, pc.APIKey, pc.APISecret) - simulationMode := livekit.SimulationMode_SIMULATION_MODE_TEXT - if cmd.Bool("audio") { - simulationMode = livekit.SimulationMode_SIMULATION_MODE_AUDIO - } - simCfg := &simulateConfig{ ctx: ctx, client: simClient, From 7416a5e3efb7bda8e52f170fa79573f29733c517 Mon Sep 17 00:00:00 2001 From: Jason Lernerman Date: Tue, 18 Aug 2026 15:34:30 -0400 Subject: [PATCH 2/2] simulate: audio is a subcommand, with impairment flags --- cmd/lk/simulate.go | 60 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index 371d1ae1..960d4e69 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -71,12 +71,9 @@ var simulateCommand = &cli.Command{ return nil, nil }, Action: func(ctx context.Context, cmd *cli.Command) error { - mode := livekit.SimulationMode_SIMULATION_MODE_TEXT - if cmd.Bool("audio") { - mode = livekit.SimulationMode_SIMULATION_MODE_AUDIO - } - return runSimulate(ctx, cmd, mode) + return runSimulate(ctx, cmd, livekit.SimulationMode_SIMULATION_MODE_TEXT) }, + Commands: []*cli.Command{simulateAudioCommand}, Flags: []cli.Flag{ &cli.IntFlag{ Name: "num-simulations", @@ -91,10 +88,6 @@ var simulateCommand = &cli.Command{ Name: "scenarios", Usage: "Path to a scenarios `FILE` (yaml). If omitted, scenarios are generated from the agent's source", }, - &cli.BoolFlag{ - Name: "audio", - Usage: "Simulate speech-to-speech interactions using the agent's full audio pipeline. By default, simulations run in text-only mode.", - }, &cli.BoolFlag{ Name: "yes", Aliases: []string{"y"}, @@ -115,6 +108,34 @@ var simulateCommand = &cli.Command{ }, } +// simulateAudioCommand inherits every flag on `simulate`: flags there are +// persistent (cli.FlagBase.Local defaults to false), so they parse on either +// side of the subcommand name. +var simulateAudioCommand = &cli.Command{ + Name: "audio", + Usage: "Simulate speech-to-speech interactions using the agent's full audio pipeline", + Description: "Options on lk agent simulate apply here too, e.g. --scenarios and --agent-name.", + ArgsUsage: "[entrypoint]", + HideHelpCommand: true, + Action: func(ctx context.Context, cmd *cli.Command) error { + return runSimulate(ctx, cmd, livekit.SimulationMode_SIMULATION_MODE_AUDIO) + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "background-noise", + Usage: "Mix ambient noise into the simulated user's audio", + }, + &cli.BoolFlag{ + Name: "low-quality-microphone", + Usage: "Publish the simulated user's audio as a low-quality microphone would capture it", + }, + &cli.BoolFlag{ + Name: "packet-loss", + Usage: "Drop packets from the simulated user's audio track", + }, + }, +} + // writeGeneratedScenariosTemp writes a generated run's scenarios to a temp // scenarios.yaml; "" when the run carries none. func writeGeneratedScenariosTemp(run *livekit.SimulationRun) (string, error) { @@ -195,6 +216,12 @@ type simulateConfig struct { viewModeRunID string // non-empty when --view opens a pre-existing run liveAgent bool // --agent-name: run against an already-running agent, don't spawn one warnings []string // config-level warnings surfaced at setup (e.g. ignored flags) + + // impairments on the simulated user's audio, set only in SIMULATION_MODE_AUDIO + backgroundNoise bool + lowQualityMicrophone bool + packetLoss bool + // TODO (steveyoon): add agent deployment support // agentDeployment string } @@ -393,6 +420,12 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S warnings: simulateConfigWarnings(mode, numSimulations), } + if simulationMode == livekit.SimulationMode_SIMULATION_MODE_AUDIO { + simCfg.backgroundNoise = cmd.Bool("background-noise") + simCfg.lowQualityMicrophone = cmd.Bool("low-quality-microphone") + simCfg.packetLoss = cmd.Bool("packet-loss") + } + if !isInteractive() { return runSimulateCI(ctx, simCfg) } @@ -523,9 +556,12 @@ func startSimulationAgent(c *simulateConfig, forwardOutput io.Writer) (*AgentPro func createSimulationRun(ctx context.Context, c *simulateConfig) (string, *livekit.PresignedPostRequest, error) { req := &livekit.SimulationRun_Create_Request{ - AgentName: c.agentName, - NumSimulations: c.numSimulations, - Mode: c.simulationMode, + AgentName: c.agentName, + NumSimulations: c.numSimulations, + Mode: c.simulationMode, + BackgroundNoise: c.backgroundNoise, + LowQualityMicrophone: c.lowQualityMicrophone, + PacketLoss: c.packetLoss, } if c.concurrency > 0 { req.Concurrency = &c.concurrency