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
11 changes: 9 additions & 2 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,14 +644,19 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
buildContext, cancel := context.WithTimeout(ctx, buildTimeout)
defer cancel()
regions := []string{region}
agentID, err := agentsClient.RegisterAgent(buildContext, secrets, regions)
created, err := agentsClient.AgentClient.CreateAgent(buildContext, &lkproto.CreateAgentRequest{
Secrets: secrets,
Regions: regions,
})
if err != nil {
if twerr, ok := err.(twirp.Error); ok {
return fmt.Errorf("unable to create agent: %s", twerr.Msg())
}
return fmt.Errorf("unable to create agent: %w", err)
}
agentID := created.AgentId
lkConfig.Agent.ID = agentID
lkConfig.Agent.Name = created.AgentName
if err := lkConfig.SaveTOMLFile(workingDir, tomlFilename); err != nil {
return err
}
Expand Down Expand Up @@ -706,6 +711,7 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
}

lkConfig.Agent.ID = resp.AgentId
lkConfig.Agent.Name = resp.AgentName
if err := lkConfig.SaveTOMLFile(workingDir, tomlFilename); err != nil {
return err
}
Expand Down Expand Up @@ -800,7 +806,8 @@ func createAgentConfig(ctx context.Context, cmd *cli.Command) error {
agent := response.Agents[0]
lkConfig := config.NewLiveKitTOML(matches[1])
lkConfig.Agent = &config.LiveKitTOMLAgentConfig{
ID: agent.AgentId,
ID: agent.AgentId,
Name: agent.AgentName,
}

if err := lkConfig.SaveTOMLFile(workingDir, tomlFilename); err != nil {
Expand Down
147 changes: 113 additions & 34 deletions cmd/lk/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import (
"encoding/json"
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -207,15 +209,21 @@ type simulateConfig struct {
concurrency int32
mode simulateMode
simulationMode livekit.SimulationMode
agentName string
projectDir string
projectType agentfs.ProjectType
entrypoint string
scenarioGroup *livekit.ScenarioGroup
scenariosPath string // path to the --scenarios file (empty when generating from source)
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)
// agentName is the agent under test from livekit.toml, the run's identity
// in the dashboard. dispatchAgentName is the name jobs are dispatched to
// when it differs: the throwaway name a locally spawned worker registers
// under (it must not collide with the deployed agent), or the live agent's
// name when --agent-name targets an already-running agent.
agentName string
dispatchAgentName string
projectDir string
projectType agentfs.ProjectType
entrypoint string
scenarioGroup *livekit.ScenarioGroup
scenariosPath string // path to the --scenarios file (empty when generating from source)
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
Expand Down Expand Up @@ -276,6 +284,58 @@ func loadScenarioGroup(path string) (*livekit.ScenarioGroup, error) {
return group, nil
}

// tomlAgentName returns [agent] name from the livekit.toml in dir, or "" when
// the file or the field is absent.
func tomlAgentName(dir string) (string, error) {
lkToml, exists, err := config.LoadTOMLFile(dir, tomlFilename)
if !exists {
return "", nil
}
if err != nil || lkToml.Agent == nil {
return "", err
}
return lkToml.Agent.Name, nil
}

// agentNameInSource matches the literal agent name an agent registers under:
// Python `agent_name="x"` and JS `agentName: "x"`.
var agentNameInSource = regexp.MustCompile(`(?:agent_name\s*=|agentName\s*:)\s*["'` + "`" + `]([^"'` + "`" + `]+)["'` + "`" + `]`)

// suggestAgentName returns the agent name found in the project's source, or a
// placeholder when there is none.
func suggestAgentName(projectDir string) string {
found := ""
filepath.WalkDir(projectDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || found != "" {
return filepath.SkipAll
}
if d.IsDir() {
switch d.Name() {
case "node_modules", ".venv", "venv", ".git", "dist", "__pycache__":
return filepath.SkipDir
}
return nil
}
switch filepath.Ext(path) {
case ".py", ".ts", ".tsx", ".js", ".mjs", ".cjs":
default:
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return nil
}
if m := agentNameInSource.FindSubmatch(b); m != nil {
found = string(m[1])
}
return nil
})
if found == "" {
return "my-agent"
}
return found
}

func generateAgentName() string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 8)
Expand Down Expand Up @@ -329,12 +389,13 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S
scenariosPath := cmd.String("scenarios")

var (
agentName string
projectDir string
projectType agentfs.ProjectType
entrypoint string
liveAgent bool
err error
agentName string
dispatchAgentName string
projectDir string
projectType agentfs.ProjectType
entrypoint string
liveAgent bool
err error
)

// --agent-name (even empty) means: run against an already-running agent,
Expand All @@ -345,16 +406,30 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S
return fmt.Errorf("--agent-name requires --scenarios (no source to generate scenarios from when running against a live agent)")
}
liveAgent = true
agentName = liveAgentName
dispatchAgentName = liveAgentName
// a livekit.toml in the working directory names the agent under test;
// without one the live agent's own name is already stable.
if tomlName, _ := tomlAgentName("."); tomlName != "" {
agentName = tomlName
} else {
agentName = liveAgentName
}
} else if runID != "" {
// --view opens a pre-existing run: nothing is spawned, so no agent
// project or entrypoint is needed.
} else {
agentName = generateAgentName()
dispatchAgentName = generateAgentName()
projectDir, projectType, err = agentfs.DetectProjectRoot(".")
if err != nil {
return err
}
agentName, err = tomlAgentName(projectDir)
if err != nil {
return err
}
if agentName == "" {
return fmt.Errorf("%s must name the agent under test so its runs can be grouped in the dashboard: run `lk agent config` for a cloud agent, or add\n\n[agent]\nname = %q\n\nfor a self-hosted one", tomlFilename, suggestAgentName(projectDir))
}

entrypointArg := cmd.Args().First()

Expand Down Expand Up @@ -402,22 +477,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,
dispatchAgentName: dispatchAgentName,
projectDir: projectDir,
projectType: projectType,
entrypoint: entrypoint,
scenarioGroup: scenarioGroup,
scenariosPath: scenariosPath,
viewModeRunID: runID,
liveAgent: liveAgent,
warnings: simulateConfigWarnings(mode, numSimulations),
}

if simulationMode == livekit.SimulationMode_SIMULATION_MODE_AUDIO {
Expand Down Expand Up @@ -544,7 +620,7 @@ func startSimulationAgent(c *simulateConfig, forwardOutput io.Writer) (*AgentPro
Env: []string{
// register under the dispatch name regardless of any agent_name
// hardcoded in the user's code
"LIVEKIT_AGENT_NAME_OVERRIDE=" + c.agentName,
"LIVEKIT_AGENT_NAME_OVERRIDE=" + c.dispatchAgentName,
"LIVEKIT_URL=" + c.pc.URL,
"LIVEKIT_API_KEY=" + c.pc.APIKey,
"LIVEKIT_API_SECRET=" + c.pc.APISecret,
Expand Down Expand Up @@ -599,6 +675,9 @@ func createSimulationRun(ctx context.Context, c *simulateConfig) (string, *livek
PacketLoss: c.packetLoss,
Ci: ciFromEnv(),
}
if c.dispatchAgentName != "" && c.dispatchAgentName != c.agentName {
req.DispatchAgentName = &c.dispatchAgentName
}
if c.concurrency > 0 {
req.Concurrency = &c.concurrency
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/google/go-querystring v1.2.0
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.20.0
github.com/livekit/protocol v1.51.1-0.20260908073808-6cde54c87840
github.com/livekit/protocol v1.51.1-0.20260910200714-4c114a8ae833
github.com/livekit/server-sdk-go/v2 v2.18.2-0.20260904062056-1da58cd7b795
github.com/mattn/go-isatty v0.0.22
github.com/moby/moby/client v0.4.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095 h1:BcliKAXoMhl/nWmzQweQ5kmh4Qqagxl4s3Z5pvM/7AY=
github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
github.com/livekit/protocol v1.51.1-0.20260908073808-6cde54c87840 h1:I9hIEcud1mJzaN0uycftHJwdwBpD0A7OaheEMbJuNm0=
github.com/livekit/protocol v1.51.1-0.20260908073808-6cde54c87840/go.mod h1:zxowkRnQlJ2VMn6ZyinXMDi985wcKXuWNeXmEERqFAs=
github.com/livekit/protocol v1.51.1-0.20260910200714-4c114a8ae833 h1:B7gceJdKBUf+8w89WCzb1P6dj5cEb9hffafC42iHVBc=
github.com/livekit/protocol v1.51.1-0.20260910200714-4c114a8ae833/go.mod h1:zxowkRnQlJ2VMn6ZyinXMDi985wcKXuWNeXmEERqFAs=
github.com/livekit/psrpc v0.7.6 h1:YG07lUMTtf+eaYI2goT9zcVZ0kGJNWN1K6ETNFtv1HQ=
github.com/livekit/psrpc v0.7.6/go.mod h1:DMw15RO7x5XmcgfwzWJYk2In605kx+wu1QRVbPfzf8M=
github.com/livekit/server-sdk-go/v2 v2.18.2-0.20260904062056-1da58cd7b795 h1:0gljvZ5rt8vSLgoyaQl3ocD9D6RQt4Iyn4kXjVr4un0=
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/livekit.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type LiveKitTOMLProjectConfig struct {

type LiveKitTOMLAgentConfig struct {
ID string `toml:"id"`
// Identity of the agent under test in simulation runs; self-hosted agents
// set it by hand.
Name string `toml:"name"`
}

func NewLiveKitTOML(forSubdomain string) *LiveKitTOML {
Expand Down
Loading