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
1 change: 1 addition & 0 deletions autocomplete/fish_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcomma
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l view -r -d 'Open a pre-existing simulation'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l export -r -d 'Print the run with run `ID` and its exact per-job chat contexts as JSON. Nothing is run or polled: the run must already be finished'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l agent-name -r -d 'Run against an already-running agent instead of spawning one locally. Pass the registered `NAME`, or "" to target the project\'s default agent (the one that auto-joins every room). Requires --scenarios.'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l deployment -s d -r -d 'deployment of the agent to dispatch to (leave empty for production)'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l help -s h -d 'show help'
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate; and not __fish_seen_subcommand_from audio' -a 'audio' -d 'Simulate speech-to-speech interactions using the agent\'s full audio pipeline'
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate; and __fish_seen_subcommand_from audio' -f -l background-noise -d 'Mix ambient noise into the simulated user\'s audio'
Expand Down
51 changes: 32 additions & 19 deletions cmd/lk/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ var simulateCommand = &cli.Command{
Name: "agent-name",
Usage: "Run against an already-running agent instead of spawning one locally. Pass the registered `NAME`, or \"\" to target the project's default agent (the one that auto-joins every room). Requires --scenarios.",
},
&cli.StringFlag{
Name: "deployment",
Usage: "deployment of the agent to dispatch to (leave empty for production)",
Aliases: []string{"d"},
},
},
}

Expand All @@ -114,7 +119,7 @@ var simulateCommand = &cli.Command{
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.",
Description: "Options on lk agent simulate apply here too, e.g. --scenarios, --agent-name, and --deployment.",
ArgsUsage: "[entrypoint]",
HideHelpCommand: true,
Action: func(ctx context.Context, cmd *cli.Command) error {
Expand Down Expand Up @@ -222,8 +227,9 @@ type simulateConfig struct {
lowQualityMicrophone bool
packetLoss bool

// TODO (steveyoon): add agent deployment support
// agentDeployment string
// Cloud AgentDispatch.deployment. Empty = production. Only meaningful
// with --agent-name; a locally spawned worker is not a Cloud deployment.
agentDeployment string
}

type simulateMode int
Expand Down Expand Up @@ -339,6 +345,11 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S

// --agent-name (even empty) means: run against an already-running agent,
// don't spawn one. https://docs.livekit.io/agents/server/agent-dispatch/#automatic
agentDeployment := cmd.String("deployment")
if cmd.IsSet("deployment") && agentDeployment != "" && !cmd.IsSet("agent-name") {
return fmt.Errorf("--deployment requires --agent-name (a locally spawned worker is not a Cloud deployment)")
}

if cmd.IsSet("agent-name") {
// nothing is spawned, so there's no source to generate scenarios from.
if scenariosPath == "" {
Expand Down Expand Up @@ -402,22 +413,23 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S
simClient := lksdk.NewAgentSimulationClient(serverURL, pc.APIKey, pc.APISecret)

simCfg := &simulateConfig{
ctx: ctx,
client: simClient,
pc: pc,
numSimulations: numSimulations,
concurrency: concurrency,
mode: mode,
simulationMode: simulationMode,
agentName: agentName,
projectDir: projectDir,
projectType: projectType,
entrypoint: entrypoint,
scenarioGroup: scenarioGroup,
scenariosPath: scenariosPath,
viewModeRunID: runID,
liveAgent: liveAgent,
warnings: simulateConfigWarnings(mode, numSimulations),
ctx: ctx,
client: simClient,
pc: pc,
numSimulations: numSimulations,
concurrency: concurrency,
mode: mode,
simulationMode: simulationMode,
agentName: agentName,
projectDir: projectDir,
projectType: projectType,
entrypoint: entrypoint,
scenarioGroup: scenarioGroup,
scenariosPath: scenariosPath,
viewModeRunID: runID,
liveAgent: liveAgent,
agentDeployment: agentDeployment,
warnings: simulateConfigWarnings(mode, numSimulations),
}

if simulationMode == livekit.SimulationMode_SIMULATION_MODE_AUDIO {
Expand Down Expand Up @@ -570,6 +582,7 @@ func createSimulationRun(ctx context.Context, c *simulateConfig) (string, *livek
LowQualityMicrophone: c.lowQualityMicrophone,
PacketLoss: c.packetLoss,
}
setSimulationCreateDeployment(req, c.agentDeployment)
if c.concurrency > 0 {
req.Concurrency = &c.concurrency
}
Expand Down
44 changes: 44 additions & 0 deletions cmd/lk/simulate_deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2025 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/reflect/protoreflect"
)

// simulationCreateDeploymentField is SimulationRun.Create.Request.deployment.
// Empty/unset = production, matching CreateAgentDispatchRequest.deployment.
const simulationCreateDeploymentField protowire.Number = 14

// setSimulationCreateDeployment writes deployment onto the create request.
// Prefer the generated field when this CLI's protocol module has it; otherwise
// emit protobuf field 14 as unknown bytes so Cloud can pin AgentDispatch before
// a protocol module bump lands here.
func setSimulationCreateDeployment(req *livekit.SimulationRun_Create_Request, deployment string) {
if req == nil || deployment == "" {
return
}
msg := req.ProtoReflect()
if fd := msg.Descriptor().Fields().ByName("deployment"); fd != nil {
msg.Set(fd, protoreflect.ValueOfString(deployment))
return
}
var b []byte
b = protowire.AppendTag(b, simulationCreateDeploymentField, protowire.BytesType)
b = protowire.AppendString(b, deployment)
msg.SetUnknown(append(msg.GetUnknown(), b...))
}
59 changes: 59 additions & 0 deletions cmd/lk/simulate_deployment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2025 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"testing"

"github.com/livekit/protocol/livekit"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
)

func TestSetSimulationCreateDeploymentEmitsField14(t *testing.T) {
req := &livekit.SimulationRun_Create_Request{AgentName: "myudda-agent"}
setSimulationCreateDeployment(req, "staging")

raw, err := proto.Marshal(req)
require.NoError(t, err)

var got string
rest := raw
for len(rest) > 0 {
num, typ, n := protowire.ConsumeTag(rest)
require.Greater(t, n, 0)
rest = rest[n:]
if typ == protowire.BytesType {
val, n := protowire.ConsumeBytes(rest)
require.Greater(t, n, 0)
rest = rest[n:]
if num == simulationCreateDeploymentField {
got = string(val)
}
continue
}
skip := protowire.ConsumeFieldValue(num, typ, rest)
require.Greater(t, skip, 0)
rest = rest[skip:]
}
require.Equal(t, "staging", got)
}

func TestSetSimulationCreateDeploymentEmptyIsOmitted(t *testing.T) {
req := &livekit.SimulationRun_Create_Request{AgentName: "myudda-agent"}
setSimulationCreateDeployment(req, "")
require.Empty(t, req.ProtoReflect().GetUnknown())
}
Loading