feat(coderd): add task prompt modification endpoint (#20811)

This PR adds the backend implementation for modifying task prompts. Part
of https://github.com/coder/internal/issues/1084

## Changes

- New `UpdateTaskPrompt` database query to update task prompts
- New PATCH `/api/v2/tasks/{task}/prompt` endpoint

## Notes

This is part 1 of a 2-part PR stack. The frontend UI will be added in a
follow-up PR based on this branch
(https://github.com/coder/coder/pull/20812).

---

🤖 PR was written by Claude Sonnet 4.5 Thinking using [Coder
Mux](https://github.com/coder/cmux) and reviewed by a human 👩
This commit is contained in:
Danielle Maywood
2025-11-25 11:13:32 +00:00
committed by GitHub
parent afd40436f0
commit 82f525baf3
18 changed files with 659 additions and 37 deletions
+88 -4
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"slices"
"strings"
"time"
"github.com/google/uuid"
@@ -500,7 +501,7 @@ func (api *API) convertTasks(ctx context.Context, requesterID uuid.UUID, dbTasks
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param task path string true "Task ID" format(uuid)
// @Param task path string true "Task ID, or task name"
// @Success 200 {object} codersdk.Task
// @Router /api/experimental/tasks/{user}/{task} [get]
//
@@ -578,7 +579,7 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param task path string true "Task ID" format(uuid)
// @Param task path string true "Task ID, or task name"
// @Success 202 "Task deletion initiated"
// @Router /api/experimental/tasks/{user}/{task} [delete]
//
@@ -646,13 +647,96 @@ func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusAccepted)
}
// @Summary Update AI task input
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
// @ID update-task-input
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param task path string true "Task ID, or task name"
// @Param request body codersdk.UpdateTaskInputRequest true "Update task input request"
// @Success 204
// @Router /api/experimental/tasks/{user}/{task}/input [patch]
//
// EXPERIMENTAL: This endpoint is experimental and not guaranteed to be stable.
// taskUpdateInput allows modifying a task's prompt before the agent executes it.
func (api *API) taskUpdateInput(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
task = httpmw.TaskParam(r)
auditor = api.Auditor.Load()
taskResourceInfo = audit.AdditionalFields{}
)
aReq, commitAudit := audit.InitRequest[database.TaskTable](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
AdditionalFields: taskResourceInfo,
})
defer commitAudit()
aReq.Old = task.TaskTable()
aReq.UpdateOrganizationID(task.OrganizationID)
var req codersdk.UpdateTaskInputRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if strings.TrimSpace(req.Input) == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Task input is required.",
})
return
}
var updatedTask database.TaskTable
if err := api.Database.InTx(func(tx database.Store) error {
task, err := tx.GetTaskByID(ctx, task.ID)
if err != nil {
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Failed to fetch task.",
Detail: err.Error(),
})
}
if task.Status != database.TaskStatusPaused {
return httperror.NewResponseError(http.StatusConflict, codersdk.Response{
Message: "Unable to update task input, task must be paused.",
Detail: "Please stop the task's workspace before updating the input.",
})
}
updatedTask, err = tx.UpdateTaskPrompt(ctx, database.UpdateTaskPromptParams{
ID: task.ID,
Prompt: req.Input,
})
if err != nil {
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Failed to update task input.",
Detail: err.Error(),
})
}
return nil
}, nil); err != nil {
httperror.WriteResponseError(ctx, rw, err)
return
}
aReq.New = updatedTask
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
}
// @Summary Send input to AI task
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
// @ID send-task-input
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param task path string true "Task ID" format(uuid)
// @Param task path string true "Task ID, or task name"
// @Param request body codersdk.TaskSendRequest true "Task input request"
// @Success 204 "Input sent successfully"
// @Router /api/experimental/tasks/{user}/{task}/send [post]
@@ -726,7 +810,7 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Experimental
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
// @Param task path string true "Task ID" format(uuid)
// @Param task path string true "Task ID, or task name"
// @Success 200 {object} codersdk.TaskLogsResponse
// @Router /api/experimental/tasks/{user}/{task}/logs [get]
//
+205
View File
@@ -23,6 +23,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/coderd/util/slice"
@@ -738,6 +739,210 @@ func TestTasks(t *testing.T) {
require.Equal(t, http.StatusBadGateway, sdkErr.StatusCode())
})
})
t.Run("UpdateInput", func(t *testing.T) {
tests := []struct {
name string
disableProvisioner bool
transition database.WorkspaceTransition
cancelTransition bool
deleteTask bool
taskInput string
wantStatus codersdk.TaskStatus
wantErr string
wantErrStatusCode int
}{
{
name: "TaskStatusInitializing",
// We want to disable the provisioner so that the task
// never gets provisioned (ensuring it stays in Initializing).
disableProvisioner: true,
taskInput: "Valid prompt",
wantStatus: codersdk.TaskStatusInitializing,
wantErr: "Unable to update",
wantErrStatusCode: http.StatusConflict,
},
{
name: "TaskStatusPaused",
transition: database.WorkspaceTransitionStop,
taskInput: "Valid prompt",
wantStatus: codersdk.TaskStatusPaused,
},
{
name: "TaskStatusError",
transition: database.WorkspaceTransitionStart,
cancelTransition: true,
taskInput: "Valid prompt",
wantStatus: codersdk.TaskStatusError,
wantErr: "Unable to update",
wantErrStatusCode: http.StatusConflict,
},
{
name: "EmptyPrompt",
transition: database.WorkspaceTransitionStop,
// We want to ensure an empty prompt is rejected.
taskInput: "",
wantStatus: codersdk.TaskStatusPaused,
wantErr: "Task input is required.",
wantErrStatusCode: http.StatusBadRequest,
},
{
name: "TaskDeleted",
transition: database.WorkspaceTransitionStop,
deleteTask: true,
taskInput: "Valid prompt",
wantErr: httpapi.ResourceNotFoundResponse.Message,
wantErrStatusCode: http.StatusNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
client, provisioner := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
template := createAITemplate(t, client, user)
if tt.disableProvisioner {
provisioner.Close()
}
// Given: We create a task
exp := codersdk.NewExperimentalClient(client)
task, err := exp.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: "initial prompt",
})
require.NoError(t, err)
require.True(t, task.WorkspaceID.Valid, "task should have a workspace ID")
if !tt.disableProvisioner {
// Given: The Task is running
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Given: We transition the task's workspace
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, tt.transition)
if tt.cancelTransition {
// Given: We cancel the workspace build
err := client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
// Then: We expect it to be canceled
build, err = client.WorkspaceBuild(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, codersdk.WorkspaceStatusCanceled, build.Status)
} else {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
}
}
if tt.deleteTask {
err = exp.DeleteTask(ctx, codersdk.Me, task.ID)
require.NoError(t, err)
} else {
// Given: Task has expected status
task, err = exp.TaskByID(ctx, task.ID)
require.NoError(t, err)
require.Equal(t, tt.wantStatus, task.Status)
}
// When: We attempt to update the task input
err = exp.UpdateTaskInput(ctx, task.OwnerName, task.ID, codersdk.UpdateTaskInputRequest{
Input: tt.taskInput,
})
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
if tt.wantErrStatusCode != 0 {
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, tt.wantErrStatusCode, apiErr.StatusCode())
}
if !tt.deleteTask {
// Then: We expect the input to **not** be updated
task, err = exp.TaskByID(ctx, task.ID)
require.NoError(t, err)
require.NotEqual(t, tt.taskInput, task.InitialPrompt)
}
} else {
require.NoError(t, err)
if !tt.deleteTask {
// Then: We expect the input to be updated
task, err = exp.TaskByID(ctx, task.ID)
require.NoError(t, err)
require.Equal(t, tt.taskInput, task.InitialPrompt)
}
}
})
}
t.Run("NonExistentTask", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitShort)
exp := codersdk.NewExperimentalClient(client)
// Attempt to update prompt for non-existent task
err := exp.UpdateTaskInput(ctx, user.UserID.String(), uuid.New(), codersdk.UpdateTaskInputRequest{
Input: "Should fail",
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
t.Run("UnauthorizedUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
anotherUser, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
template := createAITemplate(t, client, user)
// Create a task as the first user
exp := codersdk.NewExperimentalClient(client)
task, err := exp.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: template.ActiveVersionID,
Input: "initial prompt",
})
require.NoError(t, err)
require.True(t, task.WorkspaceID.Valid)
// Wait for workspace to complete
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
// Attempt to update prompt as another user should fail with 404 Not Found
otherExp := codersdk.NewExperimentalClient(anotherUser)
err = otherExp.UpdateTaskInput(ctx, task.OwnerName, task.ID, codersdk.UpdateTaskInputRequest{
Input: "Should fail - unauthorized",
})
require.Error(t, err)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
})
})
}
func TestTasksCreate(t *testing.T) {
+56 -8
View File
@@ -228,8 +228,7 @@ const docTemplate = `{
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -265,8 +264,7 @@ const docTemplate = `{
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -279,6 +277,50 @@ const docTemplate = `{
}
}
},
"/api/experimental/tasks/{user}/{task}/input": {
"patch": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": [
"Experimental"
],
"summary": "Update AI task input",
"operationId": "update-task-input",
"parameters": [
{
"type": "string",
"description": "Username, user ID, or 'me' for the authenticated user",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
},
{
"description": "Update task input request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateTaskInputRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/api/experimental/tasks/{user}/{task}/logs": {
"get": {
"security": [
@@ -301,8 +343,7 @@ const docTemplate = `{
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -340,8 +381,7 @@ const docTemplate = `{
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -19025,6 +19065,14 @@ const docTemplate = `{
}
}
},
"codersdk.UpdateTaskInputRequest": {
"type": "object",
"properties": {
"input": {
"type": "string"
}
}
},
"codersdk.UpdateTemplateACL": {
"type": "object",
"properties": {
+54 -8
View File
@@ -198,8 +198,7 @@
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -233,8 +232,7 @@
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -247,6 +245,48 @@
}
}
},
"/api/experimental/tasks/{user}/{task}/input": {
"patch": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Experimental"],
"summary": "Update AI task input",
"operationId": "update-task-input",
"parameters": [
{
"type": "string",
"description": "Username, user ID, or 'me' for the authenticated user",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
},
{
"description": "Update task input request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateTaskInputRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/api/experimental/tasks/{user}/{task}/logs": {
"get": {
"security": [
@@ -267,8 +307,7 @@
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -304,8 +343,7 @@
},
{
"type": "string",
"format": "uuid",
"description": "Task ID",
"description": "Task ID, or task name",
"name": "task",
"in": "path",
"required": true
@@ -17452,6 +17490,14 @@
}
}
},
"codersdk.UpdateTaskInputRequest": {
"type": "object",
"properties": {
"input": {
"type": "string"
}
}
},
"codersdk.UpdateTemplateACL": {
"type": "object",
"properties": {
+1
View File
@@ -1036,6 +1036,7 @@ func New(options *Options) *API {
r.Use(httpmw.ExtractTaskParam(options.Database))
r.Get("/", api.taskGet)
r.Delete("/", api.taskDelete)
r.Patch("/input", api.taskUpdateInput)
r.Post("/send", api.taskSend)
r.Get("/logs", api.taskLogs)
})
+15
View File
@@ -5130,6 +5130,21 @@ func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg
return q.db.UpdateTailnetPeerStatusByCoordinator(ctx, arg)
}
func (q *querier) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
// An actor is allowed to update the prompt of a task if they have
// permission to update the task (same as UpdateTaskWorkspaceID).
task, err := q.db.GetTaskByID(ctx, arg.ID)
if err != nil {
return database.TaskTable{}, err
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil {
return database.TaskTable{}, err
}
return q.db.UpdateTaskPrompt(ctx, arg)
}
func (q *querier) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
// An actor is allowed to update the workspace ID of a task if they are the
// owner of the task and workspace or have the appropriate permissions.
+16
View File
@@ -2457,6 +2457,22 @@ func (s *MethodTestSuite) TestTasks() {
check.Args(arg).Asserts(task, policy.ActionUpdate, ws, policy.ActionUpdate).Returns(database.TaskTable{})
}))
s.Run("UpdateTaskPrompt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
task := testutil.Fake(s.T(), faker, database.Task{})
arg := database.UpdateTaskPromptParams{
ID: task.ID,
Prompt: "Updated prompt text",
}
// Create a copy of the task with the updated prompt
updatedTask := task
updatedTask.Prompt = arg.Prompt
dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes()
dbm.EXPECT().UpdateTaskPrompt(gomock.Any(), arg).Return(updatedTask.TaskTable(), nil).AnyTimes()
check.Args(arg).Asserts(task, policy.ActionUpdate).Returns(updatedTask.TaskTable())
}))
s.Run("GetTaskByWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
task := testutil.Fake(s.T(), faker, database.Task{})
task.WorkspaceID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
@@ -3154,6 +3154,13 @@ func (m queryMetricsStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Cont
return r0
}
func (m queryMetricsStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
start := time.Now()
r0, r1 := m.s.UpdateTaskPrompt(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateTaskPrompt").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
start := time.Now()
r0, r1 := m.s.UpdateTaskWorkspaceID(ctx, arg)
+15
View File
@@ -6770,6 +6770,21 @@ func (mr *MockStoreMockRecorder) UpdateTailnetPeerStatusByCoordinator(ctx, arg a
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTailnetPeerStatusByCoordinator", reflect.TypeOf((*MockStore)(nil).UpdateTailnetPeerStatusByCoordinator), ctx, arg)
}
// UpdateTaskPrompt mocks base method.
func (m *MockStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateTaskPrompt", ctx, arg)
ret0, _ := ret[0].(database.TaskTable)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateTaskPrompt indicates an expected call of UpdateTaskPrompt.
func (mr *MockStoreMockRecorder) UpdateTaskPrompt(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskPrompt", reflect.TypeOf((*MockStore)(nil).UpdateTaskPrompt), ctx, arg)
}
// UpdateTaskWorkspaceID mocks base method.
func (m *MockStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
m.ctrl.T.Helper()
+21 -4
View File
@@ -132,11 +132,28 @@ func (w ConnectionLog) RBACObject() rbac.Object {
return obj
}
// TaskTable converts a Task to it's reduced version.
// A more generalized solution is to use json marshaling to
// consistently keep these two structs in sync.
// That would be a lot of overhead, and a more costly unit test is
// written to make sure these match up.
func (t Task) TaskTable() TaskTable {
return TaskTable{
ID: t.ID,
OrganizationID: t.OrganizationID,
OwnerID: t.OwnerID,
Name: t.Name,
WorkspaceID: t.WorkspaceID,
TemplateVersionID: t.TemplateVersionID,
TemplateParameters: t.TemplateParameters,
Prompt: t.Prompt,
CreatedAt: t.CreatedAt,
DeletedAt: t.DeletedAt,
}
}
func (t Task) RBACObject() rbac.Object {
return rbac.ResourceTask.
WithID(t.ID).
WithOwner(t.OwnerID.String()).
InOrg(t.OrganizationID)
return t.TaskTable().RBACObject()
}
func (t TaskTable) RBACObject() rbac.Object {
@@ -58,6 +58,45 @@ func TestWorkspaceTableConvert(t *testing.T) {
"To resolve this, go to the 'func (w Workspace) WorkspaceTable()' and ensure all fields are converted.")
}
// TestTaskTableConvert verifies all task fields are converted
// when reducing a `Task` to a `TaskTable`.
// This test is a guard rail to prevent developer oversight mistakes.
func TestTaskTableConvert(t *testing.T) {
t.Parallel()
staticRandoms := &testutil.Random{
String: func() string { return "foo" },
Bool: func() bool { return true },
Int: func() int64 { return 500 },
Uint: func() uint64 { return 126 },
Float: func() float64 { return 3.14 },
Complex: func() complex128 { return 6.24 },
Time: func() time.Time {
return time.Date(2020, 5, 2, 5, 19, 21, 30, time.UTC)
},
}
// Copies the approach taken by TestWorkspaceTableConvert.
//
// If you use 'PopulateStruct' to create 2 tasks, using the same
// "random" values for each type. Then they should be identical.
//
// So if 'task.TaskTable()' was missing any fields in its
// conversion, the comparison would fail.
var task Task
err := testutil.PopulateStruct(&task, staticRandoms)
require.NoError(t, err)
var subset TaskTable
err = testutil.PopulateStruct(&subset, staticRandoms)
require.NoError(t, err)
require.Equal(t, task.TaskTable(), subset,
"'task.TaskTable()' is not missing at least 1 field when converting to 'TaskTable'. "+
"To resolve this, go to the 'func (t Task) TaskTable()' and ensure all fields are converted.")
}
// TestAuditLogsQueryConsistency ensures that GetAuditLogsOffset and CountAuditLogs
// have identical WHERE clauses to prevent filtering inconsistencies.
// This test is a guard rail to prevent developer oversight mistakes.
+1
View File
@@ -686,6 +686,7 @@ type sqlcQuerier interface {
UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error
UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error)
UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error
UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error)
UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error)
UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error
UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error
+34
View File
@@ -13425,6 +13425,40 @@ func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task
return items, nil
}
const updateTaskPrompt = `-- name: UpdateTaskPrompt :one
UPDATE
tasks
SET
prompt = $1::text
WHERE
id = $2::uuid
AND deleted_at IS NULL
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
`
type UpdateTaskPromptParams struct {
Prompt string `db:"prompt" json:"prompt"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) {
row := q.db.QueryRowContext(ctx, updateTaskPrompt, arg.Prompt, arg.ID)
var i TaskTable
err := row.Scan(
&i.ID,
&i.OrganizationID,
&i.OwnerID,
&i.Name,
&i.WorkspaceID,
&i.TemplateVersionID,
&i.TemplateParameters,
&i.Prompt,
&i.CreatedAt,
&i.DeletedAt,
)
return i, err
}
const updateTaskWorkspaceID = `-- name: UpdateTaskWorkspaceID :one
UPDATE
tasks
+11
View File
@@ -64,3 +64,14 @@ WHERE
id = @id::uuid
AND deleted_at IS NULL
RETURNING *;
-- name: UpdateTaskPrompt :one
UPDATE
tasks
SET
prompt = @prompt::text
WHERE
id = @id::uuid
AND deleted_at IS NULL
RETURNING *;
+22
View File
@@ -352,6 +352,28 @@ func (c *ExperimentalClient) TaskSend(ctx context.Context, user string, id uuid.
return nil
}
// UpdateTaskInputRequest is used to update a task's input.
//
// Experimental: This type is experimental and may change in the future.
type UpdateTaskInputRequest struct {
Input string `json:"input"`
}
// UpdateTaskInput updates the task's input.
//
// Experimental: This method is experimental and may change in the future.
func (c *ExperimentalClient) UpdateTaskInput(ctx context.Context, user string, id uuid.UUID, req UpdateTaskInputRequest) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/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
}
// TaskLogType indicates the source of a task log entry.
//
// Experimental: This type is experimental and may change in the future.
+50 -13
View File
@@ -90,10 +90,10 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string(uuid) | true | Task ID |
| Name | In | Type | Required | Description |
|--------|------|--------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string | true | Task ID, or task name |
### Example responses
@@ -121,10 +121,10 @@ curl -X DELETE http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{ta
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string(uuid) | true | Task ID |
| Name | In | Type | Required | Description |
|--------|------|--------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string | true | Task ID, or task name |
### Responses
@@ -134,6 +134,43 @@ curl -X DELETE http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{ta
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Update AI task input
### Code samples
```shell
# Example request using curl
curl -X PATCH http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}/input \
-H 'Content-Type: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`PATCH /api/experimental/tasks/{user}/{task}/input`
> Body parameter
```json
{
"input": "string"
}
```
### Parameters
| Name | In | Type | Required | Description |
|--------|------|------------------------------------------------------------------------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string | true | Task ID, or task name |
| `body` | body | [codersdk.UpdateTaskInputRequest](schemas.md#codersdkupdatetaskinputrequest) | true | Update task input request |
### Responses
| Status | Meaning | Description | Schema |
|--------|-----------------------------------------------------------------|-------------|--------|
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get AI task logs
### Code samples
@@ -149,10 +186,10 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}
### Parameters
| Name | In | Type | Required | Description |
|--------|------|--------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string(uuid) | true | Task ID |
| Name | In | Type | Required | Description |
|--------|------|--------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string | true | Task ID, or task name |
### Example responses
@@ -192,7 +229,7 @@ curl -X POST http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task
| Name | In | Type | Required | Description |
|--------|------|----------------------------------------------------------------|----------|-------------------------------------------------------|
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
| `task` | path | string(uuid) | true | Task ID |
| `task` | path | string | true | Task ID, or task name |
| `body` | body | [codersdk.TaskSendRequest](schemas.md#codersdktasksendrequest) | true | Task input request |
### Responses
+14
View File
@@ -9198,6 +9198,20 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|---------|-----------------|----------|--------------|-------------|
| `roles` | array of string | false | | |
## codersdk.UpdateTaskInputRequest
```json
{
"input": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|---------|--------|----------|--------------|-------------|
| `input` | string | false | | |
## codersdk.UpdateTemplateACL
```json
+10
View File
@@ -5371,6 +5371,16 @@ export interface UpdateRoles {
readonly roles: readonly string[];
}
// From codersdk/aitasks.go
/**
* UpdateTaskInputRequest is used to update a task's input.
*
* Experimental: This type is experimental and may change in the future.
*/
export interface UpdateTaskInputRequest {
readonly input: string;
}
// From codersdk/templates.go
export interface UpdateTemplateACL {
/**