From 119c7d718cd6de06db2060e21af269d5e1b28e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:29:30 -0700 Subject: [PATCH 1/9] simulate: full per-scenario dump only in CI, report file elsewhere --- cmd/lk/simulate_ci.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index 2bae9f0b..e99f8f02 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -173,7 +173,20 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { // --- Results --- - report.Results(run, agent) + if os.Getenv("CI") != "" { + report.Results(run, agent) + } else { + // This path also serves humans whose stdin/stdout merely isn't a TTY + // (pipes, task runners): keep the terminal to counts and pointers, + // the per-scenario transcripts go to a report file like the TUI's. + if f, err := os.CreateTemp("", "lk-simulate-report-*.txt"); err == nil { + writeRunResults(asciiWriter{f}, run, agent) + f.Close() + out.Statusf("Run report: %s", f.Name()) + } + total, _, passed, failedN := simulationJobCounts(run) + fmt.Fprintf(out.ResultWriter(), "%d total, %d passed, %d failed\n", total, passed, failedN) + } if brokenAgent && agent != nil { writeBrokenAgentNote(out.WarnWriter(), agent) @@ -185,10 +198,14 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { _, _, _, failed := simulationJobCounts(run) if failed > 0 || run.Status == livekit.SimulationRun_STATUS_FAILED { + errPrefix := "" + if os.Getenv("CI") != "" { + errPrefix = "::error::" + } if failed > 0 { - out.Resultf("::error::%d simulation(s) failed\n", failed) + out.Resultf("%s%d simulation(s) failed\n", errPrefix, failed) } else { - out.Resultf("::error::Simulation run failed: %s\n", run.Error) + out.Resultf("%sSimulation run failed: %s\n", errPrefix, run.Error) } if run.Status == livekit.SimulationRun_STATUS_FAILED && len(run.Jobs) == 0 { return fmt.Errorf("simulation failed: %s", run.Error) From d19b54abbbed9dd667efb068ea2a9ca8a7941f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:31:33 -0700 Subject: [PATCH 2/9] extract CI detection into util.InCI --- cmd/lk/simulate.go | 2 +- cmd/lk/simulate_ci.go | 6 ++++-- pkg/util/env.go | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 pkg/util/env.go diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index 4b3cfc7a..c991f6dd 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -369,7 +369,7 @@ func runSimulate(ctx context.Context, cmd *cli.Command) error { } func isInteractive() bool { - if os.Getenv("CI") != "" { + if util.InCI() { return false } return isatty.IsTerminal(os.Stdin.Fd()) && isatty.IsTerminal(os.Stdout.Fd()) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index e99f8f02..985b0378 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -26,6 +26,8 @@ import ( "github.com/livekit/protocol/livekit" agent "github.com/livekit/protocol/livekit/agent" + + "github.com/livekit/livekit-cli/v2/pkg/util" ) type toggleWriter struct { @@ -173,7 +175,7 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { // --- Results --- - if os.Getenv("CI") != "" { + if util.InCI() { report.Results(run, agent) } else { // This path also serves humans whose stdin/stdout merely isn't a TTY @@ -199,7 +201,7 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { _, _, _, failed := simulationJobCounts(run) if failed > 0 || run.Status == livekit.SimulationRun_STATUS_FAILED { errPrefix := "" - if os.Getenv("CI") != "" { + if util.InCI() { errPrefix = "::error::" } if failed > 0 { diff --git a/pkg/util/env.go b/pkg/util/env.go new file mode 100644 index 00000000..449f7ed2 --- /dev/null +++ b/pkg/util/env.go @@ -0,0 +1,23 @@ +// Copyright 2026 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 util + +import "os" + +// InCI reports whether the process runs under a CI system; GitHub Actions, +// GitLab, CircleCI, Travis, and most others set CI. +func InCI() bool { + return os.Getenv("CI") != "" +} From 6a9f6d45ab8e033ba973c7b10947ca3f42ebf128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:36:22 -0700 Subject: [PATCH 3/9] gate the dump on terminal interactivity instead of CI env --- cmd/lk/simulate.go | 2 +- cmd/lk/simulate_ci.go | 12 +++++------- pkg/util/env.go | 23 ----------------------- 3 files changed, 6 insertions(+), 31 deletions(-) delete mode 100644 pkg/util/env.go diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index c991f6dd..4b3cfc7a 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -369,7 +369,7 @@ func runSimulate(ctx context.Context, cmd *cli.Command) error { } func isInteractive() bool { - if util.InCI() { + if os.Getenv("CI") != "" { return false } return isatty.IsTerminal(os.Stdin.Fd()) && isatty.IsTerminal(os.Stdout.Fd()) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index 985b0378..fd667701 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -26,8 +26,6 @@ import ( "github.com/livekit/protocol/livekit" agent "github.com/livekit/protocol/livekit/agent" - - "github.com/livekit/livekit-cli/v2/pkg/util" ) type toggleWriter struct { @@ -175,12 +173,12 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { // --- Results --- - if util.InCI() { + if !out.Interactive() { report.Results(run, agent) } else { - // This path also serves humans whose stdin/stdout merely isn't a TTY - // (pipes, task runners): keep the terminal to counts and pointers, - // the per-scenario transcripts go to a report file like the TUI's. + // A terminal is watching; we just couldn't open the TUI (e.g. stdin + // isn't a TTY). Keep it to counts and pointers, the per-scenario + // transcripts go to a report file like the TUI's. if f, err := os.CreateTemp("", "lk-simulate-report-*.txt"); err == nil { writeRunResults(asciiWriter{f}, run, agent) f.Close() @@ -201,7 +199,7 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { _, _, _, failed := simulationJobCounts(run) if failed > 0 || run.Status == livekit.SimulationRun_STATUS_FAILED { errPrefix := "" - if util.InCI() { + if !out.Interactive() { errPrefix = "::error::" } if failed > 0 { diff --git a/pkg/util/env.go b/pkg/util/env.go deleted file mode 100644 index 449f7ed2..00000000 --- a/pkg/util/env.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2026 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 util - -import "os" - -// InCI reports whether the process runs under a CI system; GitHub Actions, -// GitLab, CircleCI, Travis, and most others set CI. -func InCI() bool { - return os.Getenv("CI") != "" -} From 28f0fbaa84a2261cd7d2184d75cb334f3bed8bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:39:37 -0700 Subject: [PATCH 4/9] drop broken-agent redump on TUI exit --- cmd/lk/simulate_tui.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index c064d552..b4fee01a 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -52,10 +52,8 @@ func runSimulateTUI(config *simulateConfig) error { }() if agentProc := m.launcher.Stop(); agentProc != nil { - if m.brokenAgent { - writeBrokenAgentNote(out.WarnWriter(), agentProc) - fmt.Fprintln(out.WarnWriter()) - } + // A broken agent was already shown in the TUI and lands in the + // report file; the terminal only gets the pointers below. if agentProc.LogPath != "" { out.Statusf("Agent logs: %s", agentProc.LogPath) } From 2354adf20cafb2b4966e06bbb5f3f51af8f5ba5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:41:00 -0700 Subject: [PATCH 5/9] Revert "drop broken-agent redump on TUI exit" This reverts commit 28f0fbaa84a2261cd7d2184d75cb334f3bed8bd0. --- cmd/lk/simulate_tui.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/lk/simulate_tui.go b/cmd/lk/simulate_tui.go index b4fee01a..c064d552 100644 --- a/cmd/lk/simulate_tui.go +++ b/cmd/lk/simulate_tui.go @@ -52,8 +52,10 @@ func runSimulateTUI(config *simulateConfig) error { }() if agentProc := m.launcher.Stop(); agentProc != nil { - // A broken agent was already shown in the TUI and lands in the - // report file; the terminal only gets the pointers below. + if m.brokenAgent { + writeBrokenAgentNote(out.WarnWriter(), agentProc) + fmt.Fprintln(out.WarnWriter()) + } if agentProc.LogPath != "" { out.Statusf("Agent logs: %s", agentProc.LogPath) } From 96bc558c33d83c79d55ced0f09a351cd50cf54c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:42:09 -0700 Subject: [PATCH 6/9] print the failure line only on interactive terminals --- cmd/lk/simulate_ci.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index fd667701..4a606595 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -198,14 +198,14 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { _, _, _, failed := simulationJobCounts(run) if failed > 0 || run.Status == livekit.SimulationRun_STATUS_FAILED { - errPrefix := "" - if !out.Interactive() { - errPrefix = "::error::" - } - if failed > 0 { - out.Resultf("%s%d simulation(s) failed\n", errPrefix, failed) - } else { - out.Resultf("%sSimulation run failed: %s\n", errPrefix, run.Error) + // non-interactive output already carries the failures in the full + // dump, and the returned error prints either way + if out.Interactive() { + if failed > 0 { + out.Resultf("%d simulation(s) failed\n", failed) + } else { + out.Resultf("Simulation run failed: %s\n", run.Error) + } } if run.Status == livekit.SimulationRun_STATUS_FAILED && len(run.Jobs) == 0 { return fmt.Errorf("simulation failed: %s", run.Error) From 62a1542cb570793cc09917705c10a4f766f567a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:45:27 -0700 Subject: [PATCH 7/9] move shared result formatting into simulate_report.go --- cmd/lk/simulate_ci.go | 165 -------------------------------------- cmd/lk/simulate_report.go | 164 +++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 165 deletions(-) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index 4a606595..34c6b6ea 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -20,12 +20,10 @@ import ( "io" "os" "os/signal" - "strings" "sync/atomic" "time" "github.com/livekit/protocol/livekit" - agent "github.com/livekit/protocol/livekit/agent" ) type toggleWriter struct { @@ -215,166 +213,3 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { return nil } - -// writeRunResults writes the per-job results and the run summary, with GitHub -// group markers (a useful delimiter outside GitHub too). -func writeRunResults(w io.Writer, run *livekit.SimulationRun, ap *AgentProcess) { - if run == nil { - return - } - summary := decodeRunSummary(run) - - if run.Status == livekit.SimulationRun_STATUS_FAILED && len(run.Jobs) == 0 { - fmt.Fprintf(w, "✗ Simulation failed: %s\n", run.Error) - return - } - - for i, job := range run.Jobs { - icon := "⏺" - switch job.Status { - case livekit.SimulationRun_Job_STATUS_COMPLETED: - icon = "✓" - case livekit.SimulationRun_Job_STATUS_FAILED: - icon = "✗" - } - - label := job.Label - if label == "" { - label = fmt.Sprintf("Job %d", i+1) - } - - fmt.Fprintf(w, "::group::%s %s (%s)\n", icon, label, job.Id) - - if job.Instructions != "" { - fmt.Fprintln(w, "Instructions:") - for line := range strings.SplitSeq(job.Instructions, "\n") { - fmt.Fprintf(w, " %s\n", line) - } - } - - if job.AgentExpectations != "" { - fmt.Fprintln(w, "Expected:") - for line := range strings.SplitSeq(job.AgentExpectations, "\n") { - fmt.Fprintf(w, " %s\n", line) - } - } - - if job.Error != "" { - if job.Status == livekit.SimulationRun_Job_STATUS_COMPLETED { - fmt.Fprintf(w, "Result: %s\n", job.Error) - } else { - fmt.Fprintf(w, "Error: %s\n", job.Error) - } - } - - if summary != nil && summary.ChatHistory != nil { - writeChatHistory(w, summary.ChatHistory[job.Id]) - } - - if ap != nil && job.RoomName != "" { - logs := ap.RecentRoomLogs(0, job.RoomName) - if len(logs) > 0 { - fmt.Fprintln(w, "Logs:") - for _, line := range logs { - fmt.Fprintf(w, " %s\n", ansiEscapeRe.ReplaceAllString(line, "")) - } - } - } - - fmt.Fprintln(w, "::endgroup::") - - if job.Status == livekit.SimulationRun_Job_STATUS_FAILED { - firstLine, _, _ := strings.Cut(job.Error, "\n") - fmt.Fprintf(w, "::error::Job %d failed: %s\n", i+1, firstLine) - } - } - - if summary != nil { - writeRunSummary(w, run, summary) - } else { - msg := "The summary for this run is not available" - if run.Error != "" { - msg = run.Error - } - fmt.Fprintln(w) - fmt.Fprintln(w, "⚠ "+msg) - } -} - -func writeRunSummary(w io.Writer, run *livekit.SimulationRun, summary *livekit.SimulationRunSummary) { - total, _, passed, failed := simulationJobCounts(run) - - fmt.Fprintln(w) - fmt.Fprintln(w, "::group::Summary") - fmt.Fprintf(w, "%d total, %d passed, %d failed\n", total, passed, failed) - - if summary.GoingWell != "" { - fmt.Fprintln(w) - fmt.Fprintln(w, "Going well:") - for line := range strings.SplitSeq(summary.GoingWell, "\n") { - fmt.Fprintf(w, " %s\n", line) - } - } - - if summary.ToImprove != "" { - fmt.Fprintln(w) - fmt.Fprintln(w, "To improve:") - for line := range strings.SplitSeq(summary.ToImprove, "\n") { - fmt.Fprintf(w, " %s\n", line) - } - } - - if len(summary.Issues) > 0 { - fmt.Fprintln(w) - fmt.Fprintln(w, "Issues:") - for i, issue := range summary.Issues { - fmt.Fprintf(w, " %d. %s\n", i+1, issue.Description) - if issue.Suggestion != "" { - fmt.Fprintf(w, " Suggestion: %s\n", issue.Suggestion) - } - } - } - - fmt.Fprintln(w, "::endgroup::") -} - -func writeChatHistory(w io.Writer, chatCtx *agent.ChatContext) { - if chatCtx == nil || len(chatCtx.Items) == 0 { - return - } - fmt.Fprintln(w, "Transcript:") - for _, item := range chatCtx.Items { - switch v := item.Item.(type) { - case *agent.ChatContext_ChatItem_Message: - msg := v.Message - text := chatMessageText(msg) - if text == "" { - continue - } - switch msg.Role { - case agent.ChatRole_USER: - fmt.Fprintf(w, " ● You\n") - case agent.ChatRole_ASSISTANT: - fmt.Fprintf(w, " ● Agent\n") - default: - fmt.Fprintf(w, " ● %s\n", msg.Role) - } - for tl := range strings.SplitSeq(text, "\n") { - fmt.Fprintf(w, " %s\n", tl) - } - case *agent.ChatContext_ChatItem_FunctionCall: - fc := v.FunctionCall - fmt.Fprintf(w, " [call] %s(%s)\n", fc.Name, fc.Arguments) - case *agent.ChatContext_ChatItem_FunctionCallOutput: - fco := v.FunctionCallOutput - label := "output" - if fco.IsError { - label = "error" - } - fmt.Fprintf(w, " [%s] %s -> %s\n", label, fco.Name, fco.Output) - case *agent.ChatContext_ChatItem_AgentHandoff: - h := v.AgentHandoff - fmt.Fprintf(w, " [handoff] -> %s\n", h.NewAgentId) - } - } -} diff --git a/cmd/lk/simulate_report.go b/cmd/lk/simulate_report.go index 5fa78d06..db196777 100644 --- a/cmd/lk/simulate_report.go +++ b/cmd/lk/simulate_report.go @@ -22,6 +22,7 @@ import ( "time" "github.com/livekit/protocol/livekit" + agent "github.com/livekit/protocol/livekit/agent" ) // simLog is the single source of the plain-text simulation output, shared by @@ -144,6 +145,169 @@ func (a asciiWriter) Write(p []byte) (int, error) { return len(p), err } +// writeRunResults writes the per-job results and the run summary, with GitHub +// group markers (a useful delimiter outside GitHub too). +func writeRunResults(w io.Writer, run *livekit.SimulationRun, ap *AgentProcess) { + if run == nil { + return + } + summary := decodeRunSummary(run) + + if run.Status == livekit.SimulationRun_STATUS_FAILED && len(run.Jobs) == 0 { + fmt.Fprintf(w, "✗ Simulation failed: %s\n", run.Error) + return + } + + for i, job := range run.Jobs { + icon := "⏺" + switch job.Status { + case livekit.SimulationRun_Job_STATUS_COMPLETED: + icon = "✓" + case livekit.SimulationRun_Job_STATUS_FAILED: + icon = "✗" + } + + label := job.Label + if label == "" { + label = fmt.Sprintf("Job %d", i+1) + } + + fmt.Fprintf(w, "::group::%s %s (%s)\n", icon, label, job.Id) + + if job.Instructions != "" { + fmt.Fprintln(w, "Instructions:") + for line := range strings.SplitSeq(job.Instructions, "\n") { + fmt.Fprintf(w, " %s\n", line) + } + } + + if job.AgentExpectations != "" { + fmt.Fprintln(w, "Expected:") + for line := range strings.SplitSeq(job.AgentExpectations, "\n") { + fmt.Fprintf(w, " %s\n", line) + } + } + + if job.Error != "" { + if job.Status == livekit.SimulationRun_Job_STATUS_COMPLETED { + fmt.Fprintf(w, "Result: %s\n", job.Error) + } else { + fmt.Fprintf(w, "Error: %s\n", job.Error) + } + } + + if summary != nil && summary.ChatHistory != nil { + writeChatHistory(w, summary.ChatHistory[job.Id]) + } + + if ap != nil && job.RoomName != "" { + logs := ap.RecentRoomLogs(0, job.RoomName) + if len(logs) > 0 { + fmt.Fprintln(w, "Logs:") + for _, line := range logs { + fmt.Fprintf(w, " %s\n", ansiEscapeRe.ReplaceAllString(line, "")) + } + } + } + + fmt.Fprintln(w, "::endgroup::") + + if job.Status == livekit.SimulationRun_Job_STATUS_FAILED { + firstLine, _, _ := strings.Cut(job.Error, "\n") + fmt.Fprintf(w, "::error::Job %d failed: %s\n", i+1, firstLine) + } + } + + if summary != nil { + writeRunSummary(w, run, summary) + } else { + msg := "The summary for this run is not available" + if run.Error != "" { + msg = run.Error + } + fmt.Fprintln(w) + fmt.Fprintln(w, "⚠ "+msg) + } +} + +func writeRunSummary(w io.Writer, run *livekit.SimulationRun, summary *livekit.SimulationRunSummary) { + total, _, passed, failed := simulationJobCounts(run) + + fmt.Fprintln(w) + fmt.Fprintln(w, "::group::Summary") + fmt.Fprintf(w, "%d total, %d passed, %d failed\n", total, passed, failed) + + if summary.GoingWell != "" { + fmt.Fprintln(w) + fmt.Fprintln(w, "Going well:") + for line := range strings.SplitSeq(summary.GoingWell, "\n") { + fmt.Fprintf(w, " %s\n", line) + } + } + + if summary.ToImprove != "" { + fmt.Fprintln(w) + fmt.Fprintln(w, "To improve:") + for line := range strings.SplitSeq(summary.ToImprove, "\n") { + fmt.Fprintf(w, " %s\n", line) + } + } + + if len(summary.Issues) > 0 { + fmt.Fprintln(w) + fmt.Fprintln(w, "Issues:") + for i, issue := range summary.Issues { + fmt.Fprintf(w, " %d. %s\n", i+1, issue.Description) + if issue.Suggestion != "" { + fmt.Fprintf(w, " Suggestion: %s\n", issue.Suggestion) + } + } + } + + fmt.Fprintln(w, "::endgroup::") +} + +func writeChatHistory(w io.Writer, chatCtx *agent.ChatContext) { + if chatCtx == nil || len(chatCtx.Items) == 0 { + return + } + fmt.Fprintln(w, "Transcript:") + for _, item := range chatCtx.Items { + switch v := item.Item.(type) { + case *agent.ChatContext_ChatItem_Message: + msg := v.Message + text := chatMessageText(msg) + if text == "" { + continue + } + switch msg.Role { + case agent.ChatRole_USER: + fmt.Fprintf(w, " ● You\n") + case agent.ChatRole_ASSISTANT: + fmt.Fprintf(w, " ● Agent\n") + default: + fmt.Fprintf(w, " ● %s\n", msg.Role) + } + for tl := range strings.SplitSeq(text, "\n") { + fmt.Fprintf(w, " %s\n", tl) + } + case *agent.ChatContext_ChatItem_FunctionCall: + fc := v.FunctionCall + fmt.Fprintf(w, " [call] %s(%s)\n", fc.Name, fc.Arguments) + case *agent.ChatContext_ChatItem_FunctionCallOutput: + fco := v.FunctionCallOutput + label := "output" + if fco.IsError { + label = "error" + } + fmt.Fprintf(w, " [%s] %s -> %s\n", label, fco.Name, fco.Output) + case *agent.ChatContext_ChatItem_AgentHandoff: + h := v.AgentHandoff + fmt.Fprintf(w, " [handoff] -> %s\n", h.NewAgentId) + } + } +} + // runReporter writes the simLog to a temp file so TUI runs leave the same // record the non-TUI mode prints. type runReporter struct { From f9d81d22308d488a0ed2ec784391ae3a83ac8db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Sun, 12 Jul 2026 16:48:27 -0700 Subject: [PATCH 8/9] drop redundant failure line, main prints the returned error --- cmd/lk/simulate_ci.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index 34c6b6ea..fe9e953c 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -194,17 +194,10 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { out.Statusf("Dashboard: %s", url) } + // the returned error is printed by main and reports the failure; the + // counts line / full dump above already carries the detail _, _, _, failed := simulationJobCounts(run) if failed > 0 || run.Status == livekit.SimulationRun_STATUS_FAILED { - // non-interactive output already carries the failures in the full - // dump, and the returned error prints either way - if out.Interactive() { - if failed > 0 { - out.Resultf("%d simulation(s) failed\n", failed) - } else { - out.Resultf("Simulation run failed: %s\n", run.Error) - } - } if run.Status == livekit.SimulationRun_STATUS_FAILED && len(run.Jobs) == 0 { return fmt.Errorf("simulation failed: %s", run.Error) } From 6211e1fb17a692044aeff1b7cb4a92f9212b849f Mon Sep 17 00:00:00 2001 From: u9g Date: Mon, 13 Jul 2026 07:08:17 -0400 Subject: [PATCH 9/9] reuse runReporter for the non-TUI interactive report file (#906) The interactive branch hand-rolled the temp-file report and omitted the broken-agent note, agent log path, and dashboard URL that the TUI's report file includes via runReporter.Finish. --- cmd/lk/simulate_ci.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/lk/simulate_ci.go b/cmd/lk/simulate_ci.go index fe9e953c..0de3880c 100644 --- a/cmd/lk/simulate_ci.go +++ b/cmd/lk/simulate_ci.go @@ -177,10 +177,9 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error { // A terminal is watching; we just couldn't open the TUI (e.g. stdin // isn't a TTY). Keep it to counts and pointers, the per-scenario // transcripts go to a report file like the TUI's. - if f, err := os.CreateTemp("", "lk-simulate-report-*.txt"); err == nil { - writeRunResults(asciiWriter{f}, run, agent) - f.Close() - out.Statusf("Run report: %s", f.Name()) + dashboardURL := simulationDashboardURL(config.pc.ProjectId, runID) + if path := newRunReporter().Finish(run, agent, brokenAgent, dashboardURL); path != "" { + out.Statusf("Run report: %s", path) } total, _, passed, failedN := simulationJobCounts(run) fmt.Fprintf(out.ResultWriter(), "%d total, %d passed, %d failed\n", total, passed, failedN)