diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index a6ec5e6f4..d722c6f6d 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -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 } @@ -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 } @@ -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 { diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index bc1eac4af..712276c12 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -20,9 +20,11 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "math/rand" "os" "path/filepath" + "regexp" "strings" "time" @@ -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 @@ -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) @@ -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, @@ -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() @@ -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 { @@ -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, @@ -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 } diff --git a/go.mod b/go.mod index f6c34cf60..89c959add 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 55c46e116..e479aed71 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/config/livekit.go b/pkg/config/livekit.go index 5fd1d44cf..bce7b6775 100644 --- a/pkg/config/livekit.go +++ b/pkg/config/livekit.go @@ -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 {