Skip to content

Commit 41de4ad

Browse files
authored
feat: add task send and logs MCP tools (coder#20230)
Closes coder/internal#776
1 parent 9bef5de commit 41de4ad

File tree

3 files changed

+380
-8
lines changed

3 files changed

+380
-8
lines changed

coderd/database/dbfake/dbfake.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,21 @@ func (b WorkspaceBuildBuilder) WithAgent(mutations ...func([]*sdkproto.Agent) []
119119
return b
120120
}
121121

122-
func (b WorkspaceBuildBuilder) WithTask() WorkspaceBuildBuilder {
122+
func (b WorkspaceBuildBuilder) WithTask(seed *sdkproto.App) WorkspaceBuildBuilder {
123123
//nolint: revive // returns modified struct
124124
b.taskAppID = uuid.New()
125+
if seed == nil {
126+
seed = &sdkproto.App{}
127+
}
125128
return b.Params(database.WorkspaceBuildParameter{
126129
Name: codersdk.AITaskPromptParameterName,
127130
Value: "list me",
128131
}).WithAgent(func(a []*sdkproto.Agent) []*sdkproto.Agent {
129132
a[0].Apps = []*sdkproto.App{
130133
{
131-
Id: b.taskAppID.String(),
132-
Slug: "vcode",
134+
Id: takeFirst(seed.Id, b.taskAppID.String()),
135+
Slug: takeFirst(seed.Slug, "vcode"),
136+
Url: takeFirst(seed.Url, ""),
133137
},
134138
}
135139
return a

codersdk/toolsdk/toolsdk.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const (
5555
ToolNameDeleteTask = "coder_delete_task"
5656
ToolNameListTasks = "coder_list_tasks"
5757
ToolNameGetTaskStatus = "coder_get_task_status"
58+
ToolNameSendTaskInput = "coder_send_task_input"
59+
ToolNameGetTaskLogs = "coder_get_task_logs"
5860
)
5961

6062
func NewDeps(client *codersdk.Client, opts ...func(*Deps)) (Deps, error) {
@@ -233,6 +235,8 @@ var All = []GenericTool{
233235
DeleteTask.Generic(),
234236
ListTasks.Generic(),
235237
GetTaskStatus.Generic(),
238+
SendTaskInput.Generic(),
239+
GetTaskLogs.Generic(),
236240
}
237241

238242
type ReportTaskArgs struct {
@@ -2033,6 +2037,97 @@ var GetTaskStatus = Tool[GetTaskStatusArgs, GetTaskStatusResponse]{
20332037
},
20342038
}
20352039

2040+
type SendTaskInputArgs struct {
2041+
TaskID string `json:"task_id"`
2042+
Input string `json:"input"`
2043+
}
2044+
2045+
var SendTaskInput = Tool[SendTaskInputArgs, codersdk.Response]{
2046+
Tool: aisdk.Tool{
2047+
Name: ToolNameSendTaskInput,
2048+
Description: `Send input to a running task.`,
2049+
Schema: aisdk.Schema{
2050+
Properties: map[string]any{
2051+
"task_id": map[string]any{
2052+
"type": "string",
2053+
"description": taskIDDescription("prompt"),
2054+
},
2055+
"input": map[string]any{
2056+
"type": "string",
2057+
"description": "The input to send to the task.",
2058+
},
2059+
},
2060+
Required: []string{"task_id", "input"},
2061+
},
2062+
},
2063+
UserClientOptional: true,
2064+
Handler: func(ctx context.Context, deps Deps, args SendTaskInputArgs) (codersdk.Response, error) {
2065+
if args.TaskID == "" {
2066+
return codersdk.Response{}, xerrors.New("task_id is required")
2067+
}
2068+
2069+
if args.Input == "" {
2070+
return codersdk.Response{}, xerrors.New("input is required")
2071+
}
2072+
2073+
expClient := codersdk.NewExperimentalClient(deps.coderClient)
2074+
id, owner, err := resolveTaskID(ctx, deps.coderClient, args.TaskID)
2075+
if err != nil {
2076+
return codersdk.Response{}, err
2077+
}
2078+
2079+
err = expClient.TaskSend(ctx, owner, id, codersdk.TaskSendRequest{
2080+
Input: args.Input,
2081+
})
2082+
if err != nil {
2083+
return codersdk.Response{}, xerrors.Errorf("send task input %q: %w", args.TaskID, err)
2084+
}
2085+
2086+
return codersdk.Response{
2087+
Message: "Input sent to task successfully.",
2088+
}, nil
2089+
},
2090+
}
2091+
2092+
type GetTaskLogsArgs struct {
2093+
TaskID string `json:"task_id"`
2094+
}
2095+
2096+
var GetTaskLogs = Tool[GetTaskLogsArgs, codersdk.TaskLogsResponse]{
2097+
Tool: aisdk.Tool{
2098+
Name: ToolNameGetTaskLogs,
2099+
Description: `Get the logs of a task.`,
2100+
Schema: aisdk.Schema{
2101+
Properties: map[string]any{
2102+
"task_id": map[string]any{
2103+
"type": "string",
2104+
"description": taskIDDescription("query"),
2105+
},
2106+
},
2107+
Required: []string{"task_id"},
2108+
},
2109+
},
2110+
UserClientOptional: true,
2111+
Handler: func(ctx context.Context, deps Deps, args GetTaskLogsArgs) (codersdk.TaskLogsResponse, error) {
2112+
if args.TaskID == "" {
2113+
return codersdk.TaskLogsResponse{}, xerrors.New("task_id is required")
2114+
}
2115+
2116+
expClient := codersdk.NewExperimentalClient(deps.coderClient)
2117+
id, owner, err := resolveTaskID(ctx, deps.coderClient, args.TaskID)
2118+
if err != nil {
2119+
return codersdk.TaskLogsResponse{}, err
2120+
}
2121+
2122+
logs, err := expClient.TaskLogs(ctx, owner, id)
2123+
if err != nil {
2124+
return codersdk.TaskLogsResponse{}, xerrors.Errorf("get task logs %q: %w", args.TaskID, err)
2125+
}
2126+
2127+
return logs, nil
2128+
},
2129+
}
2130+
20362131
// normalizedNamedWorkspace normalizes the workspace name before getting the
20372132
// workspace by name.
20382133
func normalizedNamedWorkspace(ctx context.Context, client *codersdk.Client, name string) (codersdk.Workspace, error) {
@@ -2110,3 +2205,15 @@ func taskIDDescription(action string) string {
21102205
func userDescription(action string) string {
21112206
return fmt.Sprintf("Username or ID of the user for which to %s. Omit or use the `me` keyword to %s for the authenticated user.", action, action)
21122207
}
2208+
2209+
func resolveTaskID(ctx context.Context, coderClient *codersdk.Client, taskID string) (uuid.UUID, string, error) {
2210+
id, err := uuid.Parse(taskID)
2211+
if err == nil {
2212+
return id, codersdk.Me, nil
2213+
}
2214+
ws, err := normalizedNamedWorkspace(ctx, coderClient, taskID)
2215+
if err != nil {
2216+
return uuid.UUID{}, codersdk.Me, xerrors.Errorf("get task workspace %q: %w", taskID, err)
2217+
}
2218+
return ws.ID, ws.OwnerName, nil
2219+
}

0 commit comments

Comments
 (0)