Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit427b23f

Browse files
authored
feat(coderd): add tasks list and get endpoints (#19468)
Fixescoder/internal#899Example API response:```json{ "tasks": [ { "id": "a7a27450-ca16-4553-a6c5-9d6f04808569", "organization_id": "241e869f-1a61-42c9-ae1e-9d46df874058", "owner_id": "9e9b9475-0fc0-47b2-9170-a5b7b9a075ee", "name": "task-hardcore-herschel-bd08", "template_id": "accab607-bbda-4794-89ac-da3926a8b71c", "workspace_id": "a7a27450-ca16-4553-a6c5-9d6f04808569", "initial_prompt": "What directory are you in?", "status": "running", "current_state": { "timestamp": "2025-08-22T10:03:27.837842Z", "state": "working", "message": "Listed root directory contents, working directory reset", "uri": "" }, "created_at": "2025-08-22T09:21:39.697094Z", "updated_at": "2025-08-22T09:21:39.697094Z" }, { "id": "50f92138-f463-4f2b-abad-1816264b065f", "organization_id": "241e869f-1a61-42c9-ae1e-9d46df874058", "owner_id": "9e9b9475-0fc0-47b2-9170-a5b7b9a075ee", "name": "task-musing-dewdney-f058", "template_id": "accab607-bbda-4794-89ac-da3926a8b71c", "workspace_id": "50f92138-f463-4f2b-abad-1816264b065f", "initial_prompt": "What is 1 + 1?", "status": "running", "current_state": { "timestamp": "2025-08-22T09:22:33.810707Z", "state": "idle", "message": "Completed arithmetic calculation", "uri": "" }, "created_at": "2025-08-22T09:18:28.027378Z", "updated_at": "2025-08-22T09:18:28.027378Z" } ], "count": 2}```
1 parentfe36e9c commit427b23f

File tree

5 files changed

+523
-1
lines changed

5 files changed

+523
-1
lines changed

‎coderd/aitasks.go‎

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package coderd
22

33
import (
4+
"context"
45
"database/sql"
56
"errors"
67
"fmt"
78
"net/http"
89
"slices"
910
"strings"
1011

12+
"github.com/go-chi/chi/v5"
1113
"github.com/google/uuid"
14+
"golang.org/x/xerrors"
1215

1316
"cdr.dev/slog"
1417

@@ -17,6 +20,8 @@ import (
1720
"github.com/coder/coder/v2/coderd/httpapi"
1821
"github.com/coder/coder/v2/coderd/httpmw"
1922
"github.com/coder/coder/v2/coderd/rbac"
23+
"github.com/coder/coder/v2/coderd/rbac/policy"
24+
"github.com/coder/coder/v2/coderd/searchquery"
2025
"github.com/coder/coder/v2/coderd/taskname"
2126
"github.com/coder/coder/v2/codersdk"
2227
)
@@ -186,3 +191,252 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
186191
defercommitAudit()
187192
createWorkspace(ctx,aReq,apiKey.UserID,api,owner,createReq,rw,r)
188193
}
194+
195+
// tasksFromWorkspaces converts a slice of API workspaces into tasks, fetching
196+
// prompts and mapping status/state. This method enforces that only AI task
197+
// workspaces are given.
198+
func (api*API)tasksFromWorkspaces(ctx context.Context,apiWorkspaces []codersdk.Workspace) ([]codersdk.Task,error) {
199+
// Enforce that only AI task workspaces are given.
200+
for_,ws:=rangeapiWorkspaces {
201+
ifws.LatestBuild.HasAITask==nil||!*ws.LatestBuild.HasAITask {
202+
returnnil,xerrors.Errorf("workspace %s is not an AI task workspace",ws.ID)
203+
}
204+
}
205+
206+
// Fetch prompts for each workspace build and map by build ID.
207+
buildIDs:=make([]uuid.UUID,0,len(apiWorkspaces))
208+
for_,ws:=rangeapiWorkspaces {
209+
buildIDs=append(buildIDs,ws.LatestBuild.ID)
210+
}
211+
parameters,err:=api.Database.GetWorkspaceBuildParametersByBuildIDs(ctx,buildIDs)
212+
iferr!=nil {
213+
returnnil,err
214+
}
215+
promptsByBuildID:=make(map[uuid.UUID]string,len(parameters))
216+
for_,p:=rangeparameters {
217+
ifp.Name==codersdk.AITaskPromptParameterName {
218+
promptsByBuildID[p.WorkspaceBuildID]=p.Value
219+
}
220+
}
221+
222+
tasks:=make([]codersdk.Task,0,len(apiWorkspaces))
223+
for_,ws:=rangeapiWorkspaces {
224+
varcurrentState*codersdk.TaskStateEntry
225+
ifws.LatestAppStatus!=nil {
226+
currentState=&codersdk.TaskStateEntry{
227+
Timestamp:ws.LatestAppStatus.CreatedAt,
228+
State:codersdk.TaskState(ws.LatestAppStatus.State),
229+
Message:ws.LatestAppStatus.Message,
230+
URI:ws.LatestAppStatus.URI,
231+
}
232+
}
233+
tasks=append(tasks, codersdk.Task{
234+
ID:ws.ID,
235+
OrganizationID:ws.OrganizationID,
236+
OwnerID:ws.OwnerID,
237+
Name:ws.Name,
238+
TemplateID:ws.TemplateID,
239+
WorkspaceID: uuid.NullUUID{Valid:true,UUID:ws.ID},
240+
CreatedAt:ws.CreatedAt,
241+
UpdatedAt:ws.UpdatedAt,
242+
InitialPrompt:promptsByBuildID[ws.LatestBuild.ID],
243+
Status:ws.LatestBuild.Status,
244+
CurrentState:currentState,
245+
})
246+
}
247+
248+
returntasks,nil
249+
}
250+
251+
// tasksListResponse wraps a list of experimental tasks.
252+
//
253+
// Experimental: Response shape is experimental and may change.
254+
typetasksListResponsestruct {
255+
Tasks []codersdk.Task`json:"tasks"`
256+
Countint`json:"count"`
257+
}
258+
259+
// tasksList is an experimental endpoint to list AI tasks by mapping
260+
// workspaces to a task-shaped response.
261+
func (api*API)tasksList(rw http.ResponseWriter,r*http.Request) {
262+
ctx:=r.Context()
263+
apiKey:=httpmw.APIKey(r)
264+
265+
// Support standard pagination/filters for workspaces.
266+
page,ok:=ParsePagination(rw,r)
267+
if!ok {
268+
return
269+
}
270+
queryStr:=r.URL.Query().Get("q")
271+
filter,errs:=searchquery.Workspaces(ctx,api.Database,queryStr,page,api.AgentInactiveDisconnectTimeout)
272+
iflen(errs)>0 {
273+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
274+
Message:"Invalid workspace search query.",
275+
Validations:errs,
276+
})
277+
return
278+
}
279+
280+
// Ensure that we only include AI task workspaces in the results.
281+
filter.HasAITask= sql.NullBool{Valid:true,Bool:true}
282+
283+
iffilter.OwnerUsername=="me"||filter.OwnerUsername=="" {
284+
filter.OwnerID=apiKey.UserID
285+
filter.OwnerUsername=""
286+
}
287+
288+
prepared,err:=api.HTTPAuth.AuthorizeSQLFilter(r,policy.ActionRead,rbac.ResourceWorkspace.Type)
289+
iferr!=nil {
290+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
291+
Message:"Internal error preparing sql filter.",
292+
Detail:err.Error(),
293+
})
294+
return
295+
}
296+
297+
// Order with requester's favorites first, include summary row.
298+
filter.RequesterID=apiKey.UserID
299+
filter.WithSummary=true
300+
301+
workspaceRows,err:=api.Database.GetAuthorizedWorkspaces(ctx,filter,prepared)
302+
iferr!=nil {
303+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
304+
Message:"Internal error fetching workspaces.",
305+
Detail:err.Error(),
306+
})
307+
return
308+
}
309+
iflen(workspaceRows)==0 {
310+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
311+
Message:"Internal error fetching workspaces.",
312+
Detail:"Workspace summary row is missing.",
313+
})
314+
return
315+
}
316+
iflen(workspaceRows)==1 {
317+
httpapi.Write(ctx,rw,http.StatusOK,tasksListResponse{
318+
Tasks: []codersdk.Task{},
319+
Count:0,
320+
})
321+
return
322+
}
323+
324+
// Skip summary row.
325+
workspaceRows=workspaceRows[:len(workspaceRows)-1]
326+
327+
workspaces:=database.ConvertWorkspaceRows(workspaceRows)
328+
329+
// Gather associated data and convert to API workspaces.
330+
data,err:=api.workspaceData(ctx,workspaces)
331+
iferr!=nil {
332+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
333+
Message:"Internal error fetching workspace resources.",
334+
Detail:err.Error(),
335+
})
336+
return
337+
}
338+
apiWorkspaces,err:=convertWorkspaces(apiKey.UserID,workspaces,data)
339+
iferr!=nil {
340+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
341+
Message:"Internal error converting workspaces.",
342+
Detail:err.Error(),
343+
})
344+
return
345+
}
346+
347+
tasks,err:=api.tasksFromWorkspaces(ctx,apiWorkspaces)
348+
iferr!=nil {
349+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
350+
Message:"Internal error fetching task prompts and states.",
351+
Detail:err.Error(),
352+
})
353+
return
354+
}
355+
356+
httpapi.Write(ctx,rw,http.StatusOK,tasksListResponse{
357+
Tasks:tasks,
358+
Count:len(tasks),
359+
})
360+
}
361+
362+
// taskGet is an experimental endpoint to fetch a single AI task by ID
363+
// (workspace ID). It returns a synthesized task response including
364+
// prompt and status.
365+
func (api*API)taskGet(rw http.ResponseWriter,r*http.Request) {
366+
ctx:=r.Context()
367+
apiKey:=httpmw.APIKey(r)
368+
369+
idStr:=chi.URLParam(r,"id")
370+
taskID,err:=uuid.Parse(idStr)
371+
iferr!=nil {
372+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
373+
Message:fmt.Sprintf("Invalid UUID %q for task ID.",idStr),
374+
})
375+
return
376+
}
377+
378+
// For now, taskID = workspaceID, once we have a task data model in
379+
// the DB, we can change this lookup.
380+
workspaceID:=taskID
381+
workspace,err:=api.Database.GetWorkspaceByID(ctx,workspaceID)
382+
ifhttpapi.Is404Error(err) {
383+
httpapi.ResourceNotFound(rw)
384+
return
385+
}
386+
iferr!=nil {
387+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
388+
Message:"Internal error fetching workspace.",
389+
Detail:err.Error(),
390+
})
391+
return
392+
}
393+
394+
data,err:=api.workspaceData(ctx, []database.Workspace{workspace})
395+
iferr!=nil {
396+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
397+
Message:"Internal error fetching workspace resources.",
398+
Detail:err.Error(),
399+
})
400+
return
401+
}
402+
iflen(data.builds)==0||len(data.templates)==0 {
403+
httpapi.ResourceNotFound(rw)
404+
return
405+
}
406+
ifdata.builds[0].HasAITask==nil||!*data.builds[0].HasAITask {
407+
httpapi.ResourceNotFound(rw)
408+
return
409+
}
410+
411+
appStatus:= codersdk.WorkspaceAppStatus{}
412+
iflen(data.appStatuses)>0 {
413+
appStatus=data.appStatuses[0]
414+
}
415+
416+
ws,err:=convertWorkspace(
417+
apiKey.UserID,
418+
workspace,
419+
data.builds[0],
420+
data.templates[0],
421+
api.Options.AllowWorkspaceRenames,
422+
appStatus,
423+
)
424+
iferr!=nil {
425+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
426+
Message:"Internal error converting workspace.",
427+
Detail:err.Error(),
428+
})
429+
return
430+
}
431+
432+
tasks,err:=api.tasksFromWorkspaces(ctx, []codersdk.Workspace{ws})
433+
iferr!=nil {
434+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
435+
Message:"Internal error fetching task prompt and state.",
436+
Detail:err.Error(),
437+
})
438+
return
439+
}
440+
441+
httpapi.Write(ctx,rw,http.StatusOK,tasks[0])
442+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp