@@ -1,17 +1,13 @@ package coderd import ( "bytes" "context" "database/sql" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/url" "path" "slices" "strings" "time" Expand All @@ -31,6 +27,8 @@ import ( "github.com/coder/coder/v2/coderd/taskname" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" aiagentapi "github.com/coder/agentapi-sdk-go" ) // This endpoint is experimental and not guaranteed to be stable, so we're not Expand Down Expand Up @@ -629,61 +627,37 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) { } if err = api.authAndDoWithTaskSidebarAppClient(r, taskID, func(ctx context.Context, client *http.Client, appURL *url.URL) error { status , err :=agentapiDoStatusRequest(ctx, client, appURL )agentAPIClient , err :=aiagentapi.NewClient(appURL.String(), aiagentapi.WithHTTPClient( client) )if err != nil { return err } if status != "stable" { return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Task app is not ready toaccept input .", Detail:fmt.Sprintf("Status: %s", status ), Message: "Failed tocreate agentapi client .", Detail:err.Error( ), }) } var reqBody struct { Content string `json:"content"` Type string `json:"type"` } reqBody.Content = req.Input reqBody.Type = "user" req, err := agentapiNewRequest(ctx, http.MethodPost, appURL, "message", reqBody) if err != nil { return err } resp, err := client.Do(req) statusResp, err := agentAPIClient.GetStatus(ctx) if err != nil { return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Failed toreach task app endpoint .", Message: "Failed toget status from task app.", Detail: err.Error(), }) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 128)) return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Task app rejected the message.", Detail: fmt.Sprintf("Upstream status: %d; Body: %s", resp.StatusCode, body), }) } // {"$schema":"http://localhost:3284/schemas/MessageResponseBody.json","ok":true} // {"$schema":"http://localhost:3284/schemas/ErrorModel.json","title":"Unprocessable Entity","status":422,"detail":"validation failed","errors":[{"location":"body.type","value":"oof"}]} var respBody map[string]any if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { if statusResp.Status != aiagentapi.StatusStable { return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Failed to decode task app response body .", Detail:err.Error( ), Message: "Task app is not ready to accept input .", Detail:fmt.Sprintf("Status: %s", statusResp.Status ), }) } if v, ok := respBody["ok"].(bool); !ok || !v { _, err = agentAPIClient.PostMessage(ctx, aiagentapi.PostMessageParams{ Content: req.Input, Type: aiagentapi.MessageTypeUser, }) if err != nil { return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Task app rejected the message.", Detail:fmt.Sprintf("Upstream response: %v", respBody ), Detail:err.Error( ), }) } Expand All @@ -710,51 +684,29 @@ func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) { var out codersdk.TaskLogsResponse if err := api.authAndDoWithTaskSidebarAppClient(r, taskID, func(ctx context.Context, client *http.Client, appURL *url.URL) error { req, err := agentapiNewRequest(ctx, http.MethodGet, appURL, "messages", nil) if err != nil { return err } resp, err := client.Do(req) agentAPIClient, err := aiagentapi.NewClient(appURL.String(), aiagentapi.WithHTTPClient(client)) if err != nil { return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Failed toreach task app endpoint .", Message: "Failed tocreate agentapi client .", Detail: err.Error(), }) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 128)) return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Task app rejected the request.", Detail: fmt.Sprintf("Upstream status: %d; Body: %s", resp.StatusCode, body), }) } // {"$schema":"http://localhost:3284/schemas/MessagesResponseBody.json","messages":[]} var respBody struct { Messages []struct { ID int `json:"id"` Content string `json:"content"` Role string `json:"role"` Time time.Time `json:"time"` } `json:"messages"` } if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { messagesResp, err := agentAPIClient.GetMessages(ctx) if err != nil { return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Failed todecode task app response body .", Message: "Failed toget messages from task app .", Detail: err.Error(), }) } logs := make([]codersdk.TaskLogEntry, 0, len(respBody .Messages)) for _, m := rangerespBody .Messages { logs := make([]codersdk.TaskLogEntry, 0, len(messagesResp .Messages)) for _, m := rangemessagesResp .Messages { var typ codersdk.TaskLogType switchstrings.ToLower( m.Role) { case"user" : switch m.Role { caseaiagentapi.RoleUser : typ = codersdk.TaskLogTypeInput case"agent" : caseaiagentapi.RoleAgent : typ = codersdk.TaskLogTypeOutput default: return httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Expand All @@ -763,7 +715,7 @@ func (api *API) taskLogs(rw http.ResponseWriter, r *http.Request) { }) } logs = append(logs, codersdk.TaskLogEntry{ ID:m.ID , ID:int(m.Id) , Content: m.Content, Type: typ, Time: m.Time, Expand Down Expand Up @@ -903,69 +855,3 @@ func (api *API) authAndDoWithTaskSidebarAppClient( } return do(ctx, client, parsedURL) } func agentapiNewRequest(ctx context.Context, method string, appURL *url.URL, appURLPath string, body any) (*http.Request, error) { u := *appURL u.Path = path.Join(appURL.Path, appURLPath) var bodyReader io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return nil, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "Failed to marshal task app request body.", Detail: err.Error(), }) } bodyReader = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader) if err != nil { return nil, httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{ Message: "Failed to create task app request.", Detail: err.Error(), }) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") return req, nil } func agentapiDoStatusRequest(ctx context.Context, client *http.Client, appURL *url.URL) (string, error) { req, err := agentapiNewRequest(ctx, http.MethodGet, appURL, "status", nil) if err != nil { return "", err } resp, err := client.Do(req) if err != nil { return "", httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Failed to reach task app endpoint.", Detail: err.Error(), }) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Task app status returned an error.", Detail: fmt.Sprintf("Status code: %d", resp.StatusCode), }) } // {"$schema":"http://localhost:3284/schemas/StatusResponseBody.json","status":"stable"} var respBody struct { Status string `json:"status"` } if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { return "", httperror.NewResponseError(http.StatusBadGateway, codersdk.Response{ Message: "Failed to decode task app status response body.", Detail: err.Error(), }) } return respBody.Status, nil }