@@ -20,12 +20,10 @@ import (
2020 "io"
2121 "os"
2222 "os/signal"
23- "strings"
2423 "sync/atomic"
2524 "time"
2625
2726 "github.com/livekit/protocol/livekit"
28- agent "github.com/livekit/protocol/livekit/agent"
2927)
3028
3129type toggleWriter struct {
@@ -173,7 +171,19 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error {
173171
174172 // --- Results ---
175173
176- report .Results (run , agent )
174+ if ! out .Interactive () {
175+ report .Results (run , agent )
176+ } else {
177+ // A terminal is watching; we just couldn't open the TUI (e.g. stdin
178+ // isn't a TTY). Keep it to counts and pointers, the per-scenario
179+ // transcripts go to a report file like the TUI's.
180+ dashboardURL := simulationDashboardURL (config .pc .ProjectId , runID )
181+ if path := newRunReporter ().Finish (run , agent , brokenAgent , dashboardURL ); path != "" {
182+ out .Statusf ("Run report: %s" , path )
183+ }
184+ total , _ , passed , failedN := simulationJobCounts (run )
185+ fmt .Fprintf (out .ResultWriter (), "%d total, %d passed, %d failed\n " , total , passed , failedN )
186+ }
177187
178188 if brokenAgent && agent != nil {
179189 writeBrokenAgentNote (out .WarnWriter (), agent )
@@ -183,13 +193,10 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error {
183193 out .Statusf ("Dashboard: %s" , url )
184194 }
185195
196+ // the returned error is printed by main and reports the failure; the
197+ // counts line / full dump above already carries the detail
186198 _ , _ , _ , failed := simulationJobCounts (run )
187199 if failed > 0 || run .Status == livekit .SimulationRun_STATUS_FAILED {
188- if failed > 0 {
189- out .Resultf ("::error::%d simulation(s) failed\n " , failed )
190- } else {
191- out .Resultf ("::error::Simulation run failed: %s\n " , run .Error )
192- }
193200 if run .Status == livekit .SimulationRun_STATUS_FAILED && len (run .Jobs ) == 0 {
194201 return fmt .Errorf ("simulation failed: %s" , run .Error )
195202 }
@@ -198,166 +205,3 @@ func runSimulateCI(ctx context.Context, config *simulateConfig) error {
198205
199206 return nil
200207}
201-
202- // writeRunResults writes the per-job results and the run summary, with GitHub
203- // group markers (a useful delimiter outside GitHub too).
204- func writeRunResults (w io.Writer , run * livekit.SimulationRun , ap * AgentProcess ) {
205- if run == nil {
206- return
207- }
208- summary := decodeRunSummary (run )
209-
210- if run .Status == livekit .SimulationRun_STATUS_FAILED && len (run .Jobs ) == 0 {
211- fmt .Fprintf (w , "✗ Simulation failed: %s\n " , run .Error )
212- return
213- }
214-
215- for i , job := range run .Jobs {
216- icon := "⏺"
217- switch job .Status {
218- case livekit .SimulationRun_Job_STATUS_COMPLETED :
219- icon = "✓"
220- case livekit .SimulationRun_Job_STATUS_FAILED :
221- icon = "✗"
222- }
223-
224- label := job .Label
225- if label == "" {
226- label = fmt .Sprintf ("Job %d" , i + 1 )
227- }
228-
229- fmt .Fprintf (w , "::group::%s %s (%s)\n " , icon , label , job .Id )
230-
231- if job .Instructions != "" {
232- fmt .Fprintln (w , "Instructions:" )
233- for line := range strings .SplitSeq (job .Instructions , "\n " ) {
234- fmt .Fprintf (w , " %s\n " , line )
235- }
236- }
237-
238- if job .AgentExpectations != "" {
239- fmt .Fprintln (w , "Expected:" )
240- for line := range strings .SplitSeq (job .AgentExpectations , "\n " ) {
241- fmt .Fprintf (w , " %s\n " , line )
242- }
243- }
244-
245- if job .Error != "" {
246- if job .Status == livekit .SimulationRun_Job_STATUS_COMPLETED {
247- fmt .Fprintf (w , "Result: %s\n " , job .Error )
248- } else {
249- fmt .Fprintf (w , "Error: %s\n " , job .Error )
250- }
251- }
252-
253- if summary != nil && summary .ChatHistory != nil {
254- writeChatHistory (w , summary .ChatHistory [job .Id ])
255- }
256-
257- if ap != nil && job .RoomName != "" {
258- logs := ap .RecentRoomLogs (0 , job .RoomName )
259- if len (logs ) > 0 {
260- fmt .Fprintln (w , "Logs:" )
261- for _ , line := range logs {
262- fmt .Fprintf (w , " %s\n " , ansiEscapeRe .ReplaceAllString (line , "" ))
263- }
264- }
265- }
266-
267- fmt .Fprintln (w , "::endgroup::" )
268-
269- if job .Status == livekit .SimulationRun_Job_STATUS_FAILED {
270- firstLine , _ , _ := strings .Cut (job .Error , "\n " )
271- fmt .Fprintf (w , "::error::Job %d failed: %s\n " , i + 1 , firstLine )
272- }
273- }
274-
275- if summary != nil {
276- writeRunSummary (w , run , summary )
277- } else {
278- msg := "The summary for this run is not available"
279- if run .Error != "" {
280- msg = run .Error
281- }
282- fmt .Fprintln (w )
283- fmt .Fprintln (w , "⚠ " + msg )
284- }
285- }
286-
287- func writeRunSummary (w io.Writer , run * livekit.SimulationRun , summary * livekit.SimulationRunSummary ) {
288- total , _ , passed , failed := simulationJobCounts (run )
289-
290- fmt .Fprintln (w )
291- fmt .Fprintln (w , "::group::Summary" )
292- fmt .Fprintf (w , "%d total, %d passed, %d failed\n " , total , passed , failed )
293-
294- if summary .GoingWell != "" {
295- fmt .Fprintln (w )
296- fmt .Fprintln (w , "Going well:" )
297- for line := range strings .SplitSeq (summary .GoingWell , "\n " ) {
298- fmt .Fprintf (w , " %s\n " , line )
299- }
300- }
301-
302- if summary .ToImprove != "" {
303- fmt .Fprintln (w )
304- fmt .Fprintln (w , "To improve:" )
305- for line := range strings .SplitSeq (summary .ToImprove , "\n " ) {
306- fmt .Fprintf (w , " %s\n " , line )
307- }
308- }
309-
310- if len (summary .Issues ) > 0 {
311- fmt .Fprintln (w )
312- fmt .Fprintln (w , "Issues:" )
313- for i , issue := range summary .Issues {
314- fmt .Fprintf (w , " %d. %s\n " , i + 1 , issue .Description )
315- if issue .Suggestion != "" {
316- fmt .Fprintf (w , " Suggestion: %s\n " , issue .Suggestion )
317- }
318- }
319- }
320-
321- fmt .Fprintln (w , "::endgroup::" )
322- }
323-
324- func writeChatHistory (w io.Writer , chatCtx * agent.ChatContext ) {
325- if chatCtx == nil || len (chatCtx .Items ) == 0 {
326- return
327- }
328- fmt .Fprintln (w , "Transcript:" )
329- for _ , item := range chatCtx .Items {
330- switch v := item .Item .(type ) {
331- case * agent.ChatContext_ChatItem_Message :
332- msg := v .Message
333- text := chatMessageText (msg )
334- if text == "" {
335- continue
336- }
337- switch msg .Role {
338- case agent .ChatRole_USER :
339- fmt .Fprintf (w , " ● You\n " )
340- case agent .ChatRole_ASSISTANT :
341- fmt .Fprintf (w , " ● Agent\n " )
342- default :
343- fmt .Fprintf (w , " ● %s\n " , msg .Role )
344- }
345- for tl := range strings .SplitSeq (text , "\n " ) {
346- fmt .Fprintf (w , " %s\n " , tl )
347- }
348- case * agent.ChatContext_ChatItem_FunctionCall :
349- fc := v .FunctionCall
350- fmt .Fprintf (w , " [call] %s(%s)\n " , fc .Name , fc .Arguments )
351- case * agent.ChatContext_ChatItem_FunctionCallOutput :
352- fco := v .FunctionCallOutput
353- label := "output"
354- if fco .IsError {
355- label = "error"
356- }
357- fmt .Fprintf (w , " [%s] %s -> %s\n " , label , fco .Name , fco .Output )
358- case * agent.ChatContext_ChatItem_AgentHandoff :
359- h := v .AgentHandoff
360- fmt .Fprintf (w , " [handoff] -> %s\n " , h .NewAgentId )
361- }
362- }
363- }
0 commit comments