From e0ed2c61e83ce398a255b5b89304d51bcd07d845 Mon Sep 17 00:00:00 2001 From: Jack Dwyer Date: Wed, 5 Aug 2026 15:20:44 -0700 Subject: [PATCH] feat(agent): richer deploy output with deployment name and links After a successful `lk agent deploy`, report the agent name, target deployment, and live version, plus links to analytics and the agent console (deep-linked to the deployment). Resolves name/version with a single existing ListAgents call; falls back to a minimal status line if that lookup fails so a successful deploy is never reported as an error. Console deep-link now carries the deployment as a query param. Co-Authored-By: Claude Opus 4.8 --- cmd/lk/agent.go | 68 +++++++++++++++++++++++++++++++++++++++- cmd/lk/agent_run.go | 26 ++++++++++++--- cmd/lk/agent_run_test.go | 34 +++++++++++++++++--- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/cmd/lk/agent.go b/cmd/lk/agent.go index b543e406..15292cf6 100644 --- a/cmd/lk/agent.go +++ b/cmd/lk/agent.go @@ -879,10 +879,76 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("unable to deploy agent: %w", err) } - out.Status("Deployed agent") + reportDeployment(ctx, agentId, agentDeployment) return nil } +// reportDeployment prints a summary of a completed deployment — the agent name, +// the target deployment, the live version, and links to the agent details page +// and the agent console for the deployment. It resolves the name/version with a +// single ListAgents call; on any failure it falls back to the minimal status +// line so a successful deploy is never reported as a failure. +func reportDeployment(ctx context.Context, agentID, deployment string) { + targetDeployment := deployment + if targetDeployment == "" { + targetDeployment = "production" + } + + agentName := "" + version := "" + if res, err := agentsClient.ListAgents(ctx, &lkproto.ListAgentsRequest{AgentId: agentID}); err == nil { + for _, agent := range res.Agents { + for _, regionalAgent := range agent.AgentDeployments { + regionDeployment := regionalAgent.Deployment + if regionDeployment == "" { + regionDeployment = "production" + } + if regionDeployment != targetDeployment { + continue + } + if regionalAgent.AgentName != "" { + agentName = regionalAgent.AgentName + } + if regionalAgent.Version != "" { + version = regionalAgent.Version + } else if agent.Version != "" { + version = agent.Version + } + } + } + } + + summary := "Completed deployment of agent" + if agentName != "" { + summary += fmt.Sprintf(" %s", util.Accented(agentName)) + } + summary += fmt.Sprintf(" to %s", util.Accented(targetDeployment)) + if version != "" { + summary += fmt.Sprintf(" (%s)", version) + } + out.Status(summary) + + // Links use the same ws URL the deploy ran against; both are "" for + // non-cloud projects, in which case nothing is printed. + wsURL := project.URL + if link := cloudAgentURL(wsURL, agentID); link != "" { + out.Statusf("Agent details: %s", consoleLinkLabel(link)) + } + if link := cloudConsoleURL(wsURL, agentName, deployment); link != "" { + out.Statusf("Test in Agent Console: %s", consoleLinkLabel(link)) + } +} + +// consoleLinkLabel accents a URL and makes it a clickable OSC 8 hyperlink on +// interactive terminals, matching how `lk agent start` renders console links. +func consoleLinkLabel(link string) string { + label := util.Accented(link) + if out.Interactive() { + label = util.Hyperlink(link, label) + } + return label +} + func promoteAgent(ctx context.Context, cmd *cli.Command) error { agentID, err := getAgentID(ctx, cmd, workingDir, tomlFilename, false) if err != nil { diff --git a/cmd/lk/agent_run.go b/cmd/lk/agent_run.go index 7c56d20c..ac619750 100644 --- a/cmd/lk/agent_run.go +++ b/cmd/lk/agent_run.go @@ -34,16 +34,34 @@ import ( // cloudConsoleURL returns the LiveKit Cloud agents-console URL for a worker that // registered against wsURL with the given agent name, or "" when wsURL is not a // LiveKit Cloud project (e.g. a self-hosted or localhost server), in which case -// no console link is shown. -func cloudConsoleURL(wsURL, agentName string) string { +// no console link is shown. A non-empty deployment is deep-linked so the console +// targets that deployment; an empty deployment targets production. +func cloudConsoleURL(wsURL, agentName, deployment string) string { consoleHost, sub := cloudProject(wsURL) if consoleHost == "" { return "" } - return fmt.Sprintf( + link := fmt.Sprintf( "https://%s/projects/d_%s/agents/console?agentName=%s&autoStart=false", consoleHost, sub, url.QueryEscape(agentName), ) + if deployment != "" { + link += "&deployment=" + url.QueryEscape(deployment) + } + return link +} + +// cloudAgentURL returns the LiveKit Cloud analytics URL for a specific agent, or +// "" when wsURL is not a recognized LiveKit Cloud project. +func cloudAgentURL(wsURL, agentID string) string { + consoleHost, sub := cloudProject(wsURL) + if consoleHost == "" { + return "" + } + return fmt.Sprintf( + "https://%s/projects/d_%s/agents/%s", + consoleHost, sub, url.QueryEscape(agentID), + ) } // cloudProject maps a LiveKit Cloud project URL to its console host and project @@ -306,7 +324,7 @@ func runAgentDev(ctx context.Context, cmd *cli.Command) error { // agent in the browser. Printed once, even across hot reloads (link stays valid). var consoleLinkOnce sync.Once cfg.OnServerInfo = func(agentName, wsURL string) { - if link := cloudConsoleURL(wsURL, agentName); link != "" { + if link := cloudConsoleURL(wsURL, agentName, ""); link != "" { consoleLinkOnce.Do(func() { // Delay briefly so the link prints after the agent's own startup // logs rather than getting buried in them. diff --git a/cmd/lk/agent_run_test.go b/cmd/lk/agent_run_test.go index 275546a3..90e9506c 100644 --- a/cmd/lk/agent_run_test.go +++ b/cmd/lk/agent_run_test.go @@ -398,23 +398,47 @@ func TestCloudProject(t *testing.T) { func TestCloudConsoleURL(t *testing.T) { assert.Equal(t, "https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false", - cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent"), + cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent", ""), ) // staging projects point at the staging console host assert.Equal(t, "https://cloud.staging.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false", - cloudConsoleURL("wss://dztest2.staging.livekit.cloud", "my-agent"), + cloudConsoleURL("wss://dztest2.staging.livekit.cloud", "my-agent", ""), ) // empty agent name (the common dev default) still yields a usable link assert.Equal(t, "https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=&autoStart=false", - cloudConsoleURL("wss://dztest2.livekit.cloud", ""), + cloudConsoleURL("wss://dztest2.livekit.cloud", "", ""), ) // agent names are query-escaped assert.Equal(t, "https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my+agent%2F1&autoStart=false", - cloudConsoleURL("wss://dztest2.livekit.cloud", "my agent/1"), + cloudConsoleURL("wss://dztest2.livekit.cloud", "my agent/1", ""), + ) + // a non-production deployment is deep-linked via the deployment param + assert.Equal(t, + "https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false&deployment=staging", + cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent", "staging"), + ) + // deployment values are query-escaped + assert.Equal(t, + "https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false&deployment=pre%2Fprod", + cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent", "pre/prod"), + ) + // non-cloud URLs produce no link + assert.Empty(t, cloudConsoleURL("http://localhost:7880", "my-agent", "")) +} + +func TestCloudAgentURL(t *testing.T) { + assert.Equal(t, + "https://cloud.livekit.io/projects/d_dztest2/agents/CA_abc123", + cloudAgentURL("wss://dztest2.livekit.cloud", "CA_abc123"), + ) + // staging projects point at the staging console host + assert.Equal(t, + "https://cloud.staging.livekit.io/projects/d_dztest2/agents/CA_abc123", + cloudAgentURL("wss://dztest2.staging.livekit.cloud", "CA_abc123"), ) // non-cloud URLs produce no link - assert.Empty(t, cloudConsoleURL("http://localhost:7880", "my-agent")) + assert.Empty(t, cloudAgentURL("http://localhost:7880", "CA_abc123")) }