Skip to content

Commit 1b45769

Browse files
simulate: allow pointing simulation to running agent via --agent-name (#892)
* simulate: allow pointing simulation to already running agent * simulate: allow using empty agent name * simulate: --agent-name fish autocomplete * Update comment Co-authored-by: Théo Monnom <theo.monnom@outlook.com> --------- Co-authored-by: Théo Monnom <theo.monnom@outlook.com>
1 parent 9f5cef0 commit 1b45769

4 files changed

Lines changed: 118 additions & 80 deletions

File tree

autocomplete/fish_autocomplete

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcomma
208208
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l audio -d 'Simulate speech-to-speech interactions using the agent\'s full audio pipeline. By default, simulations run in text-only mode.'
209209
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l yes -s y -d 'Skip the source-upload confirmation prompt (required for non-interactive runs that generate from source)'
210210
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'
211+
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.'
211212
complete -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate' -f -l help -s h -d 'show help'
212213
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and __fish_seen_subcommand_from simulate; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
213214
complete -x -c lk -n '__fish_seen_subcommand_from agent a; and not __fish_seen_subcommand_from init create dockerfile config deploy status update restart rollback logs tail delete destroy versions list secrets update-secrets private-link start dev console daemon simulate help h' -a 'help' -d 'Shows a list of commands or help for one command'

cmd/lk/simulate.go

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ var simulateCommand = &cli.Command{
9393
Name: "view",
9494
Usage: "Open a pre-existing simulation",
9595
},
96+
&cli.StringFlag{
97+
Name: "agent-name",
98+
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.",
99+
},
96100
},
97101
}
98102

@@ -174,6 +178,7 @@ type simulateConfig struct {
174178
scenarioGroup *livekit.ScenarioGroup
175179
scenariosPath string // path to the --scenarios file (empty when generating from source)
176180
viewModeRunID string // non-empty when --view opens a pre-existing run
181+
liveAgent bool // --agent-name: run against an already-running agent, don't spawn one
177182
}
178183

179184
type simulateMode int
@@ -251,34 +256,55 @@ func runSimulate(ctx context.Context, cmd *cli.Command) error {
251256
numSimulations := int32(cmd.Int("num-simulations"))
252257
concurrency := int32(cmd.Int("concurrency"))
253258
runID := cmd.String("view")
254-
agentName := generateAgentName()
259+
liveAgentName := cmd.String("agent-name")
255260

256-
projectDir, projectType, err := agentfs.DetectProjectRoot(".")
257-
if err != nil {
258-
return err
259-
}
260-
261-
entrypointArg := cmd.Args().First()
261+
// never auto-discovered: an explicit --scenarios file is the source of
262+
// truth, otherwise scenarios are generated from the agent's source
263+
scenariosPath := cmd.String("scenarios")
262264

263-
// check if a script called "build" exists in the package.json, if so, refuse to discover the
264-
// entrypoint: build tasks usually mean the entrypoint path is nontrivial (e.g. dist/main.js)
265-
if projectType.IsNode() && entrypointArg == "" {
266-
buildTaskDoesExist, err := buildTaskExists(projectDir)
265+
var (
266+
agentName string
267+
projectDir string
268+
projectType agentfs.ProjectType
269+
entrypoint string
270+
liveAgent bool
271+
err error
272+
)
273+
274+
// --agent-name (even empty) means: run against an already-running agent,
275+
// don't spawn one. https://docs.livekit.io/agents/server/agent-dispatch/#automatic
276+
if cmd.IsSet("agent-name") {
277+
// nothing is spawned, so there's no source to generate scenarios from.
278+
if scenariosPath == "" {
279+
return fmt.Errorf("--agent-name requires --scenarios (no source to generate scenarios from when running against a live agent)")
280+
}
281+
liveAgent = true
282+
agentName = liveAgentName
283+
} else {
284+
agentName = generateAgentName()
285+
projectDir, projectType, err = agentfs.DetectProjectRoot(".")
267286
if err != nil {
268287
return err
269-
} else if buildTaskDoesExist {
270-
return fmt.Errorf("you currently have a build task in your package.json, but no entrypoint was explicitly given; so you must add an entrypoint to the simulate cli")
271288
}
272-
}
273289

274-
entrypoint, err := findEntrypoint(projectDir, entrypointArg, projectType)
275-
if err != nil {
276-
return err
277-
}
290+
entrypointArg := cmd.Args().First()
278291

279-
// never auto-discovered: an explicit --scenarios file is the source of
280-
// truth, otherwise scenarios are generated from the agent's source
281-
scenariosPath := cmd.String("scenarios")
292+
// check if a script called "build" exists in the package.json, if so, refuse to discover the
293+
// entrypoint: build tasks usually mean the entrypoint path is nontrivial (e.g. dist/main.js)
294+
if projectType.IsNode() && entrypointArg == "" {
295+
buildTaskDoesExist, err := buildTaskExists(projectDir)
296+
if err != nil {
297+
return err
298+
} else if buildTaskDoesExist {
299+
return fmt.Errorf("you currently have a build task in your package.json, but no entrypoint was explicitly given; so you must add an entrypoint to the simulate cli")
300+
}
301+
}
302+
303+
entrypoint, err = findEntrypoint(projectDir, entrypointArg, projectType)
304+
if err != nil {
305+
return err
306+
}
307+
}
282308

283309
var scenarioGroup *livekit.ScenarioGroup
284310
if scenariosPath != "" {
@@ -326,6 +352,7 @@ func runSimulate(ctx context.Context, cmd *cli.Command) error {
326352
scenarioGroup: scenarioGroup,
327353
scenariosPath: scenariosPath,
328354
viewModeRunID: runID,
355+
liveAgent: liveAgent,
329356
}
330357

331358
if !isInteractive() {

cmd/lk/simulate_ci.go

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -72,37 +72,39 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error {
7272
report := newSimLog(out.ResultWriter(), out.StatusWriter())
7373
report.BeginSetup()
7474

75-
report.StartingAgent()
76-
start := time.Now()
77-
logFwd := &toggleWriter{w: out.StatusWriter()}
78-
logFwd.enabled.Store(true)
7975
var err error
80-
agent, err = startSimulationAgent(config, logFwd)
81-
if err != nil {
82-
report.AgentStartFailed(err)
83-
report.EndSetup()
84-
return fmt.Errorf("failed to start agent: %w", err)
85-
}
76+
if !config.liveAgent {
77+
report.StartingAgent()
78+
start := time.Now()
79+
logFwd := &toggleWriter{w: out.StatusWriter()}
80+
logFwd.enabled.Store(true)
81+
agent, err = startSimulationAgent(config, logFwd)
82+
if err != nil {
83+
report.AgentStartFailed(err)
84+
report.EndSetup()
85+
return fmt.Errorf("failed to start agent: %w", err)
86+
}
8687

87-
report.WaitingForRegister()
88-
timeout := time.NewTimer(agentRegisterTimeout)
89-
defer timeout.Stop()
90-
select {
91-
case <-agent.Ready():
92-
logFwd.enabled.Store(false)
93-
report.AgentRegistered(time.Since(start))
94-
case <-agent.Done():
95-
report.EndSetup()
96-
return fmt.Errorf("the agent exited before registering.\n\n%s", agentExitDetail(agent))
97-
case <-timeout.C:
98-
report.EndSetup()
99-
return fmt.Errorf("timed out after %s waiting for the agent to register.\n\n%s", agentRegisterTimeout, agentExitDetail(agent))
100-
case <-ctx.Done():
101-
report.EndSetup()
102-
return ctx.Err()
88+
report.WaitingForRegister()
89+
timeout := time.NewTimer(agentRegisterTimeout)
90+
defer timeout.Stop()
91+
select {
92+
case <-agent.Ready():
93+
logFwd.enabled.Store(false)
94+
report.AgentRegistered(time.Since(start))
95+
case <-agent.Done():
96+
report.EndSetup()
97+
return fmt.Errorf("the agent exited before registering.\n\n%s", agentExitDetail(agent))
98+
case <-timeout.C:
99+
report.EndSetup()
100+
return fmt.Errorf("timed out after %s waiting for the agent to register.\n\n%s", agentRegisterTimeout, agentExitDetail(agent))
101+
case <-ctx.Done():
102+
report.EndSetup()
103+
return ctx.Err()
104+
}
103105
}
104106

105-
start = time.Now()
107+
start := time.Now()
106108
var presigned *livekit.PresignedPostRequest
107109
runID, presigned, err = createSimulationRun(ctx, config)
108110
if err != nil {
@@ -144,7 +146,8 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error {
144146
}
145147
out.Warnf("Warning: poll failed: %v", err)
146148
} else {
147-
// the worker is failing systemically: stop early and surface its log
149+
// the worker is failing systemically (or, in live-agent mode, the
150+
// agent never joined): stop early and surface its log
148151
if !brokenAgent && agentBroken(run, agent) {
149152
brokenAgent = true
150153
report.BrokenAgent()

cmd/lk/simulate_tui.go

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,32 @@ import (
3535
)
3636

3737
func runSimulateTUI(config *simulateConfig) error {
38-
launcher := launchSimulationAgent(config)
39-
m := newSimulateModel(config, launcher)
38+
m := newSimulateModel(config)
4039
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
4140
_, runErr := p.Run()
4241

43-
// A second ctrl+c during cleanup would kill the CLI and leak the worker
44-
// (own process group, port stays bound); escalate to SIGKILL instead.
45-
sigCh := make(chan os.Signal, 1)
46-
signal.Notify(sigCh, os.Interrupt)
47-
defer signal.Stop(sigCh)
48-
go func() {
49-
<-sigCh
50-
launcher.ForceStop()
51-
os.Exit(130)
52-
}()
53-
54-
if agentProc := launcher.Stop(); agentProc != nil {
55-
if m.brokenAgent {
56-
writeBrokenAgentNote(out.WarnWriter(), agentProc)
57-
fmt.Fprintln(out.WarnWriter())
58-
}
59-
if agentProc.LogPath != "" {
60-
out.Statusf("Agent logs: %s", agentProc.LogPath)
42+
if m.launcher != nil {
43+
// A second ctrl+c during cleanup would kill the CLI and leak the worker
44+
// (own process group, port stays bound); escalate to SIGKILL instead.
45+
sigCh := make(chan os.Signal, 1)
46+
signal.Notify(sigCh, os.Interrupt)
47+
defer signal.Stop(sigCh)
48+
go func() {
49+
<-sigCh
50+
m.launcher.ForceStop()
51+
os.Exit(130)
52+
}()
53+
54+
if agentProc := m.launcher.Stop(); agentProc != nil {
55+
if m.brokenAgent {
56+
writeBrokenAgentNote(out.WarnWriter(), agentProc)
57+
fmt.Fprintln(out.WarnWriter())
58+
}
59+
if agentProc.LogPath != "" {
60+
out.Statusf("Agent logs: %s", agentProc.LogPath)
61+
}
62+
m.agent = agentProc
6163
}
62-
m.agent = agentProc
6364
}
6465

6566
// generated scenarios are saved to a temp file so they're never lost
@@ -297,14 +298,13 @@ func (m *simulateModel) showToast(text string, ok bool) tea.Cmd {
297298
return tea.Tick(4*time.Second, func(time.Time) tea.Msg { return toastExpireMsg{id: id} })
298299
}
299300

300-
func newSimulateModel(config *simulateConfig, launcher *agentLauncher) *simulateModel {
301+
func newSimulateModel(config *simulateConfig) *simulateModel {
301302
ti := textinput.New()
302303
ti.Placeholder = "scenarios.yaml"
303304
ti.CharLimit = 128
304305
ti.Prompt = ""
305306
return &simulateModel{
306307
config: config,
307-
launcher: launcher,
308308
reporter: newRunReporter(),
309309
numSimulations: config.numSimulations,
310310
width: 80,
@@ -374,25 +374,32 @@ func (m *simulateModel) runSetup() tea.Cmd {
374374
m.steps = append(m.steps, step{label: label, status: "done"})
375375
}
376376
m.currentStep = len(m.steps)
377-
m.steps = append(m.steps,
378-
step{label: "Starting agent", status: "running"},
379-
step{label: "Creating simulation", status: "pending"},
380-
)
381-
if c.mode == modeGenerateFromSource {
382-
m.steps = append(m.steps, step{label: "Uploading source", status: "pending"})
383-
}
384377

385378
m.reporter.BeginSetup()
386379
if c.mode == modeScenarios && c.scenarioGroup != nil {
387380
m.reporter.ScenariosLoaded(c.scenarioGroup, c.scenariosPath)
388381
}
389-
m.reporter.StartingAgent()
390382

391383
ctx, cancel := context.WithCancel(c.ctx)
392384
m.setupCtx = ctx
393385
m.setupCancel = cancel
394386
m.stepStart = time.Now()
395387

388+
if c.liveAgent {
389+
m.steps = append(m.steps, step{label: "Creating simulation", status: "running"})
390+
return m.createSimulationCmd()
391+
}
392+
393+
m.steps = append(m.steps,
394+
step{label: "Starting agent", status: "running"},
395+
step{label: "Creating simulation", status: "pending"},
396+
)
397+
if c.mode == modeGenerateFromSource {
398+
m.steps = append(m.steps, step{label: "Uploading source", status: "pending"})
399+
}
400+
m.reporter.StartingAgent()
401+
402+
m.launcher = launchSimulationAgent(c)
396403
return m.startAgentCmd()
397404
}
398405

0 commit comments

Comments
 (0)