-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathaitasks.go
More file actions
423 lines (368 loc) · 14.8 KB
/
aitasks.go
File metadata and controls
423 lines (368 loc) · 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
)
// CreateTaskRequest represents the request to create a new task.
type CreateTaskRequest struct {
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"`
Input string `json:"input"`
Name string `json:"name,omitempty"`
DisplayName string `json:"display_name,omitempty"`
}
// CreateTask creates a new task.
func (c *Client) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Task, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/tasks/%s", user), request)
if err != nil {
return Task{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return Task{}, ReadBodyAsError(res)
}
var task Task
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
return Task{}, err
}
return task, nil
}
// TaskStatus represents the status of a task.
type TaskStatus string
const (
// TaskStatusPending indicates the task has been created but no workspace
// has been provisioned yet, or the workspace build job status is unknown.
TaskStatusPending TaskStatus = "pending"
// TaskStatusInitializing indicates the workspace build is pending/running,
// the agent is connecting, or apps are initializing.
TaskStatusInitializing TaskStatus = "initializing"
// TaskStatusActive indicates the task's workspace is running with a
// successful start transition, the agent is connected, and all workspace
// apps are either healthy or disabled.
TaskStatusActive TaskStatus = "active"
// TaskStatusPaused indicates the task's workspace has been stopped or
// deleted (stop/delete transition with successful job status).
TaskStatusPaused TaskStatus = "paused"
// TaskStatusUnknown indicates the task's status cannot be determined
// based on the workspace build, agent lifecycle, or app health states.
TaskStatusUnknown TaskStatus = "unknown"
// TaskStatusError indicates the task's workspace build job has failed,
// or the workspace apps are reporting unhealthy status.
TaskStatusError TaskStatus = "error"
)
func AllTaskStatuses() []TaskStatus {
return []TaskStatus{
TaskStatusPending,
TaskStatusInitializing,
TaskStatusActive,
TaskStatusPaused,
TaskStatusError,
TaskStatusUnknown,
}
}
// TaskState represents the high-level lifecycle of a task.
type TaskState string
// TaskState enums.
const (
// TaskStateWorking indicates the AI agent is actively processing work.
// Reported when the agent is performing actions or the screen is changing.
TaskStateWorking TaskState = "working"
// TaskStateIdle indicates the AI agent's screen is stable and no work
// is being performed. Reported automatically by the screen watcher.
TaskStateIdle TaskState = "idle"
// TaskStateComplete indicates the AI agent has successfully completed
// the task. Reported via the workspace app status.
TaskStateComplete TaskState = "complete"
// TaskStateFailed indicates the AI agent reported a failure state.
// Reported via the workspace app status.
TaskStateFailed TaskState = "failed"
)
// Task represents a task.
type Task struct {
ID uuid.UUID `json:"id" format:"uuid" table:"id"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid" table:"organization id"`
OwnerID uuid.UUID `json:"owner_id" format:"uuid" table:"owner id"`
OwnerName string `json:"owner_name" table:"owner name"`
OwnerAvatarURL string `json:"owner_avatar_url,omitempty" table:"owner avatar url"`
Name string `json:"name" table:"name,default_sort"`
DisplayName string `json:"display_name" table:"display_name"`
TemplateID uuid.UUID `json:"template_id" format:"uuid" table:"template id"`
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid" table:"template version id"`
TemplateName string `json:"template_name" table:"template name"`
TemplateDisplayName string `json:"template_display_name" table:"template display name"`
TemplateIcon string `json:"template_icon" table:"template icon"`
WorkspaceID uuid.NullUUID `json:"workspace_id" format:"uuid" table:"workspace id"`
WorkspaceName string `json:"workspace_name" table:"workspace name"`
WorkspaceStatus WorkspaceStatus `json:"workspace_status,omitempty" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted" table:"workspace status"`
WorkspaceBuildNumber int32 `json:"workspace_build_number,omitempty" table:"workspace build number"`
WorkspaceAgentID uuid.NullUUID `json:"workspace_agent_id" format:"uuid" table:"workspace agent id"`
WorkspaceAgentLifecycle *WorkspaceAgentLifecycle `json:"workspace_agent_lifecycle" table:"workspace agent lifecycle"`
WorkspaceAgentHealth *WorkspaceAgentHealth `json:"workspace_agent_health" table:"workspace agent health"`
WorkspaceAppID uuid.NullUUID `json:"workspace_app_id" format:"uuid" table:"workspace app id"`
InitialPrompt string `json:"initial_prompt" table:"initial prompt"`
Status TaskStatus `json:"status" enums:"pending,initializing,active,paused,unknown,error" table:"status"`
CurrentState *TaskStateEntry `json:"current_state" table:"cs,recursive_inline,empty_nil"`
CreatedAt time.Time `json:"created_at" format:"date-time" table:"created at"`
UpdatedAt time.Time `json:"updated_at" format:"date-time" table:"updated at"`
}
// TaskStateEntry represents a single entry in the task's state history.
type TaskStateEntry struct {
Timestamp time.Time `json:"timestamp" format:"date-time" table:"-"`
State TaskState `json:"state" enum:"working,idle,completed,failed" table:"state"`
Message string `json:"message" table:"message"`
URI string `json:"uri" table:"-"`
}
// TasksFilter filters the list of tasks.
type TasksFilter struct {
// Owner can be a username, UUID, or "me".
Owner string `json:"owner,omitempty"`
// Organization can be an organization name or UUID.
Organization string `json:"organization,omitempty"`
// Status filters the tasks by their task status.
Status TaskStatus `json:"status,omitempty"`
// FilterQuery allows specifying a raw filter query.
FilterQuery string `json:"filter_query,omitempty"`
}
// TaskListResponse is the response shape for tasks list.
type TasksListResponse struct {
Tasks []Task `json:"tasks"`
Count int `json:"count"`
}
func (f TasksFilter) asRequestOption() RequestOption {
return func(r *http.Request) {
var params []string
// Make sure all user input is quoted to ensure it's parsed as a single
// string.
if f.Owner != "" {
params = append(params, fmt.Sprintf("owner:%q", f.Owner))
}
if f.Organization != "" {
params = append(params, fmt.Sprintf("organization:%q", f.Organization))
}
if f.Status != "" {
params = append(params, fmt.Sprintf("status:%q", string(f.Status)))
}
if f.FilterQuery != "" {
// If custom stuff is added, just add it on here.
params = append(params, f.FilterQuery)
}
q := r.URL.Query()
q.Set("q", strings.Join(params, " "))
r.URL.RawQuery = q.Encode()
}
}
// Tasks lists all tasks belonging to the user or specified owner.
func (c *Client) Tasks(ctx context.Context, filter *TasksFilter) ([]Task, error) {
if filter == nil {
filter = &TasksFilter{}
}
res, err := c.Request(ctx, http.MethodGet, "/api/v2/tasks", nil, filter.asRequestOption())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var tres TasksListResponse
if err := json.NewDecoder(res.Body).Decode(&tres); err != nil {
return nil, err
}
return tres.Tasks, nil
}
// TaskByID fetches a single task by its ID.
// Only tasks owned by codersdk.Me are supported.
func (c *Client) TaskByID(ctx context.Context, id uuid.UUID) (Task, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/tasks/%s/%s", "me", id.String()), nil)
if err != nil {
return Task{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Task{}, ReadBodyAsError(res)
}
var task Task
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
return Task{}, err
}
return task, nil
}
// TaskByOwnerAndName fetches a single task by its owner and name.
func (c *Client) TaskByOwnerAndName(ctx context.Context, owner, ident string) (Task, error) {
if owner == "" {
owner = Me
}
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/tasks/%s/%s", owner, ident), nil)
if err != nil {
return Task{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Task{}, ReadBodyAsError(res)
}
var task Task
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
return Task{}, err
}
return task, nil
}
func splitTaskIdentifier(identifier string) (owner string, taskName string, err error) {
parts := strings.Split(identifier, "/")
switch len(parts) {
case 1:
owner = Me
taskName = parts[0]
case 2:
owner = parts[0]
taskName = parts[1]
default:
return "", "", xerrors.Errorf("invalid task identifier: %q", identifier)
}
return owner, taskName, nil
}
// TaskByIdentifier fetches and returns a task by an identifier, which may be
// either a UUID, a name (for a task owned by the current user), or a
// "user/task" combination, where user is either a username or UUID.
//
// Since there is no TaskByOwnerAndName endpoint yet, this function uses the
// list endpoint with filtering when a name is provided.
func (c *Client) TaskByIdentifier(ctx context.Context, identifier string) (Task, error) {
identifier = strings.TrimSpace(identifier)
// Try parsing as UUID first.
if taskID, err := uuid.Parse(identifier); err == nil {
return c.TaskByID(ctx, taskID)
}
// Not a UUID, treat as identifier.
owner, taskName, err := splitTaskIdentifier(identifier)
if err != nil {
return Task{}, err
}
return c.TaskByOwnerAndName(ctx, owner, taskName)
}
// DeleteTask deletes a task by its ID.
func (c *Client) DeleteTask(ctx context.Context, user string, id uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/tasks/%s/%s", user, id.String()), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusAccepted {
return ReadBodyAsError(res)
}
return nil
}
// TaskSendRequest is used to send task input to the tasks sidebar app.
type TaskSendRequest struct {
Input string `json:"input"`
}
// TaskSend submits task input to the tasks sidebar app.
func (c *Client) TaskSend(ctx context.Context, user string, id uuid.UUID, req TaskSendRequest) error {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/tasks/%s/%s/send", user, id.String()), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// UpdateTaskInputRequest is used to update a task's input.
type UpdateTaskInputRequest struct {
Input string `json:"input"`
}
// UpdateTaskInput updates the task's input.
func (c *Client) UpdateTaskInput(ctx context.Context, user string, id uuid.UUID, req UpdateTaskInputRequest) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/tasks/%s/%s/input", user, id.String()), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// PauseTaskResponse represents the response from pausing a task.
type PauseTaskResponse struct {
WorkspaceBuild *WorkspaceBuild `json:"workspace_build"`
}
// PauseTask pauses a task by stopping its workspace.
func (c *Client) PauseTask(ctx context.Context, user string, id uuid.UUID) (PauseTaskResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/tasks/%s/%s/pause", user, id.String()), nil)
if err != nil {
return PauseTaskResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusAccepted {
return PauseTaskResponse{}, ReadBodyAsError(res)
}
var resp PauseTaskResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return PauseTaskResponse{}, err
}
return resp, nil
}
// ResumeTaskResponse represents the response from resuming a task.
type ResumeTaskResponse struct {
WorkspaceBuild *WorkspaceBuild `json:"workspace_build"`
}
func (c *Client) ResumeTask(ctx context.Context, user string, id uuid.UUID) (ResumeTaskResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/tasks/%s/%s/resume", user, id.String()), nil)
if err != nil {
return ResumeTaskResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusAccepted {
return ResumeTaskResponse{}, ReadBodyAsError(res)
}
var resp ResumeTaskResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return ResumeTaskResponse{}, err
}
return resp, nil
}
// TaskLogType indicates the source of a task log entry.
type TaskLogType string
// TaskLogType enums.
const (
TaskLogTypeInput TaskLogType = "input"
TaskLogTypeOutput TaskLogType = "output"
)
// TaskLogEntry represents a single log entry for a task.
type TaskLogEntry struct {
ID int `json:"id" table:"id"`
Content string `json:"content" table:"content"`
Type TaskLogType `json:"type" enum:"input,output" table:"type"`
Time time.Time `json:"time" format:"date-time" table:"time,default_sort"`
}
// TaskLogsResponse contains task logs and metadata. When snapshot is false,
// logs are fetched live from the task app. When snapshot is true, logs are
// fetched from a stored snapshot captured during pause.
type TaskLogsResponse struct {
Logs []TaskLogEntry `json:"logs"`
Snapshot bool `json:"snapshot,omitempty"`
SnapshotAt *time.Time `json:"snapshot_at,omitempty"`
}
// TaskLogs retrieves logs from the task app.
func (c *Client) TaskLogs(ctx context.Context, user string, id uuid.UUID) (TaskLogsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/tasks/%s/%s/logs", user, id.String()), nil)
if err != nil {
return TaskLogsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return TaskLogsResponse{}, ReadBodyAsError(res)
}
var logs TaskLogsResponse
if err := json.NewDecoder(res.Body).Decode(&logs); err != nil {
return TaskLogsResponse{}, xerrors.Errorf("decoding task logs response: %w", err)
}
return logs, nil
}