@@ -786,6 +786,30 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
786786 rw .WriteHeader (http .StatusNoContent )
787787}
788788
789+ // convertAgentAPIMessagesToLogEntries converts AgentAPI messages to
790+ // TaskLogEntry format.
791+ func convertAgentAPIMessagesToLogEntries (messages []agentapisdk.Message ) ([]codersdk.TaskLogEntry , error ) {
792+ logs := make ([]codersdk.TaskLogEntry , 0 , len (messages ))
793+ for _ , m := range messages {
794+ var typ codersdk.TaskLogType
795+ switch m .Role {
796+ case agentapisdk .RoleUser :
797+ typ = codersdk .TaskLogTypeInput
798+ case agentapisdk .RoleAgent :
799+ typ = codersdk .TaskLogTypeOutput
800+ default :
801+ return nil , xerrors .Errorf ("invalid agentapi message role %q" , m .Role )
802+ }
803+ logs = append (logs , codersdk.TaskLogEntry {
804+ ID : int (m .Id ),
805+ Content : m .Content ,
806+ Type : typ ,
807+ Time : m .Time ,
808+ })
809+ }
810+ return logs , nil
811+ }
812+
789813// @Summary Get AI task logs
790814// @ID get-ai-task-logs
791815// @Security CoderSessionToken
@@ -799,8 +823,42 @@ func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) {
799823 ctx := r .Context ()
800824 task := httpmw .TaskParam (r )
801825
826+ switch task .Status {
827+ case database .TaskStatusActive :
828+ // Active tasks: fetch live logs from AgentAPI.
829+ out , err := api .fetchLiveTaskLogs (r , task )
830+ if err != nil {
831+ httperror .WriteResponseError (ctx , rw , err )
832+ return
833+ }
834+
835+ httpapi .Write (ctx , rw , http .StatusOK , out )
836+
837+ case database .TaskStatusPaused , database .TaskStatusPending , database .TaskStatusInitializing :
838+ // In pause, pending and initializing states, we attempt to fetch
839+ // the snapshot from database to provide continuity.
840+ out , err := api .fetchSnapshotTaskLogs (ctx , task .ID )
841+ if err != nil {
842+ httperror .WriteResponseError (ctx , rw , err )
843+ return
844+ }
845+
846+ httpapi .Write (ctx , rw , http .StatusOK , out )
847+
848+ default :
849+ // Cases: database.TaskStatusError, database.TaskStatusUnknown.
850+ // - Error: snapshot would be stale from previous pause.
851+ // - Unknown: cannot determine reliable state.
852+ httpapi .Write (ctx , rw , http .StatusConflict , codersdk.Response {
853+ Message : "Cannot fetch logs for task in current state." ,
854+ Detail : fmt .Sprintf ("Task status is %q." , task .Status ),
855+ })
856+ }
857+ }
858+
859+ func (api * API ) fetchLiveTaskLogs (r * http.Request , task database.Task ) (codersdk.TaskLogsResponse , error ) {
802860 var out codersdk.TaskLogsResponse
803- if err := api .authAndDoWithTaskAppClient (r , task , func (ctx context.Context , client * http.Client , appURL * url.URL ) error {
861+ err := api .authAndDoWithTaskAppClient (r , task , func (ctx context.Context , client * http.Client , appURL * url.URL ) error {
804862 agentAPIClient , err := agentapisdk .NewClient (appURL .String (), agentapisdk .WithHTTPClient (client ))
805863 if err != nil {
806864 return httperror .NewResponseError (http .StatusBadGateway , codersdk.Response {
@@ -817,35 +875,89 @@ func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) {
817875 })
818876 }
819877
820- logs := make ([]codersdk.TaskLogEntry , 0 , len (messagesResp .Messages ))
821- for _ , m := range messagesResp .Messages {
822- var typ codersdk.TaskLogType
823- switch m .Role {
824- case agentapisdk .RoleUser :
825- typ = codersdk .TaskLogTypeInput
826- case agentapisdk .RoleAgent :
827- typ = codersdk .TaskLogTypeOutput
828- default :
829- return httperror .NewResponseError (http .StatusBadGateway , codersdk.Response {
830- Message : "Invalid task app response message role." ,
831- Detail : fmt .Sprintf (`Expected "user" or "agent", got %q.` , m .Role ),
832- })
833- }
834- logs = append (logs , codersdk.TaskLogEntry {
835- ID : int (m .Id ),
836- Content : m .Content ,
837- Type : typ ,
838- Time : m .Time ,
878+ logs , err := convertAgentAPIMessagesToLogEntries (messagesResp .Messages )
879+ if err != nil {
880+ return httperror .NewResponseError (http .StatusBadGateway , codersdk.Response {
881+ Message : "Invalid task app response." ,
882+ Detail : err .Error (),
839883 })
840884 }
841- out = codersdk.TaskLogsResponse {Logs : logs }
885+
886+ out = codersdk.TaskLogsResponse {
887+ Logs : logs ,
888+ }
842889 return nil
843- }); err != nil {
844- httperror .WriteResponseError (ctx , rw , err )
845- return
890+ })
891+ return out , err
892+ }
893+
894+ func (api * API ) fetchSnapshotTaskLogs (ctx context.Context , taskID uuid.UUID ) (codersdk.TaskLogsResponse , error ) {
895+ snapshot , err := api .Database .GetTaskSnapshot (ctx , taskID )
896+ if err != nil {
897+ if httpapi .IsUnauthorizedError (err ) {
898+ return codersdk.TaskLogsResponse {}, httperror .NewResponseError (http .StatusNotFound , codersdk.Response {
899+ Message : "Resource not found." ,
900+ })
901+ }
902+ if errors .Is (err , sql .ErrNoRows ) {
903+ // No snapshot exists yet, return empty logs. Snapshot is true
904+ // because this field indicates whether the data is from the
905+ // live task app (false) or not (true). Since the task is
906+ // paused/initializing/pending, we cannot fetch live logs, so
907+ // snapshot must be true even with no snapshot data.
908+ return codersdk.TaskLogsResponse {
909+ Logs : []codersdk.TaskLogEntry {},
910+ Snapshot : true ,
911+ }, nil
912+ }
913+ return codersdk.TaskLogsResponse {}, httperror .NewResponseError (http .StatusInternalServerError , codersdk.Response {
914+ Message : "Internal error fetching task snapshot." ,
915+ Detail : err .Error (),
916+ })
917+ }
918+
919+ // Unmarshal envelope with pre-populated data field to decode once.
920+ envelope := TaskLogSnapshotEnvelope {
921+ Data : & agentapisdk.GetMessagesResponse {},
922+ }
923+ if err := json .Unmarshal (snapshot .LogSnapshot , & envelope ); err != nil {
924+ return codersdk.TaskLogsResponse {}, httperror .NewResponseError (http .StatusInternalServerError , codersdk.Response {
925+ Message : "Internal error decoding task snapshot." ,
926+ Detail : err .Error (),
927+ })
928+ }
929+
930+ // Validate snapshot format.
931+ if envelope .Format != "agentapi" {
932+ return codersdk.TaskLogsResponse {}, httperror .NewResponseError (http .StatusInternalServerError , codersdk.Response {
933+ Message : "Unsupported task snapshot format." ,
934+ Detail : fmt .Sprintf ("Expected format %q, got %q." , "agentapi" , envelope .Format ),
935+ })
936+ }
937+
938+ // Extract agentapi data from envelope (already decoded into the correct type).
939+ messagesResp , ok := envelope .Data .(* agentapisdk.GetMessagesResponse )
940+ if ! ok {
941+ return codersdk.TaskLogsResponse {}, httperror .NewResponseError (http .StatusInternalServerError , codersdk.Response {
942+ Message : "Internal error decoding snapshot data." ,
943+ Detail : "Unexpected data type in envelope." ,
944+ })
945+ }
946+
947+ // Convert agentapi messages to log entries.
948+ logs , err := convertAgentAPIMessagesToLogEntries (messagesResp .Messages )
949+ if err != nil {
950+ return codersdk.TaskLogsResponse {}, httperror .NewResponseError (http .StatusInternalServerError , codersdk.Response {
951+ Message : "Invalid snapshot data." ,
952+ Detail : err .Error (),
953+ })
846954 }
847955
848- httpapi .Write (ctx , rw , http .StatusOK , out )
956+ return codersdk.TaskLogsResponse {
957+ Logs : logs ,
958+ Snapshot : true ,
959+ SnapshotAt : ptr .Ref (snapshot .LogSnapshotCreatedAt ),
960+ }, nil
849961}
850962
851963// authAndDoWithTaskAppClient centralizes the shared logic to:
0 commit comments