From 882ed8485e034decb9f5b67a67db02dfca0b06c9 Mon Sep 17 00:00:00 2001 From: Max schwenk Date: Thu, 23 Apr 2026 16:36:26 -0400 Subject: [PATCH] feat(codersdk/toolsdk): expose template version presets to agents Adds a coder_template_version_presets tool so agents can discover presets, and a template_version_preset_id argument on the create_workspace tool so they can use one. The backend and the create_task tool already honored the preset ID; this closes the agent-facing gap. Co-Authored-By: Claude Opus 4.7 (1M context) --- codersdk/toolsdk/toolsdk.go | 58 +++++++++++++++++++++++++++---- codersdk/toolsdk/toolsdk_test.go | 59 +++++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index b6d1141a1444f..fbd0f1185f284 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -30,6 +30,7 @@ const ( ToolNameListWorkspaces = "coder_list_workspaces" ToolNameListTemplates = "coder_list_templates" ToolNameListTemplateVersionParams = "coder_template_version_parameters" + ToolNameListTemplateVersionPresets = "coder_template_version_presets" ToolNameGetAuthenticatedUser = "coder_get_authenticated_user" ToolNameCreateWorkspaceBuild = "coder_create_workspace_build" ToolNameCreateTemplateVersion = "coder_create_template_version" @@ -310,6 +311,7 @@ var All = []GenericTool{ DeleteTemplate.Generic(), ListTemplates.Generic(), ListTemplateVersionParameters.Generic(), + ListTemplateVersionPresets.Generic(), ListWorkspaces.Generic(), GetAuthenticatedUser.Generic(), GetTemplateVersionLogs.Generic(), @@ -441,10 +443,11 @@ This returns more data than list_workspaces to reduce token usage.`, } type CreateWorkspaceArgs struct { - Name string `json:"name"` - RichParameters map[string]string `json:"rich_parameters"` - TemplateVersionID string `json:"template_version_id"` - User string `json:"user"` + Name string `json:"name"` + RichParameters map[string]string `json:"rich_parameters"` + TemplateVersionID string `json:"template_version_id"` + TemplateVersionPresetID string `json:"template_version_preset_id"` + User string `json:"user"` } var CreateWorkspace = Tool[CreateWorkspaceArgs, codersdk.Workspace]{ @@ -478,6 +481,10 @@ be ready before trying to use or connect to the workspace. "type": "string", "description": "ID of the template version to create the workspace from.", }, + "template_version_preset_id": map[string]any{ + "type": "string", + "description": "Optional ID of the template version preset to create the workspace from.", + }, "name": map[string]any{ "type": "string", "description": "Name of the workspace to create.", @@ -496,6 +503,14 @@ be ready before trying to use or connect to the workspace. if err != nil { return codersdk.Workspace{}, xerrors.New("template_version_id must be a valid UUID") } + + var tvPresetID uuid.UUID + if args.TemplateVersionPresetID != "" { + tvPresetID, err = uuid.Parse(args.TemplateVersionPresetID) + if err != nil { + return codersdk.Workspace{}, xerrors.New("template_version_preset_id must be a valid UUID") + } + } if args.User == "" { args.User = codersdk.Me } @@ -506,11 +521,15 @@ be ready before trying to use or connect to the workspace. Value: v, }) } - workspace, err := deps.coderClient.CreateUserWorkspace(ctx, args.User, codersdk.CreateWorkspaceRequest{ + req := codersdk.CreateWorkspaceRequest{ TemplateVersionID: tvID, Name: args.Name, RichParameterValues: buildParams, - }) + } + if tvPresetID != uuid.Nil { + req.TemplateVersionPresetID = tvPresetID + } + workspace, err := deps.coderClient.CreateUserWorkspace(ctx, args.User, req) if err != nil { return codersdk.Workspace{}, err } @@ -626,6 +645,33 @@ var ListTemplateVersionParameters = Tool[ListTemplateVersionParametersArgs, []co }, } +var ListTemplateVersionPresets = Tool[ListTemplateVersionParametersArgs, []codersdk.Preset]{ + Tool: aisdk.Tool{ + Name: ToolNameListTemplateVersionPresets, + Description: "Get the presets for a template version. Use these to choose a template_version_preset_id for task or workspace creation.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "template_version_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"template_version_id"}, + }, + }, + MCPAnnotations: mcpReadOnlyAnnotations, + Handler: func(ctx context.Context, deps Deps, args ListTemplateVersionParametersArgs) ([]codersdk.Preset, error) { + templateVersionID, err := uuid.Parse(args.TemplateVersionID) + if err != nil { + return nil, xerrors.Errorf("template_version_id must be a valid UUID: %w", err) + } + presets, err := deps.coderClient.TemplateVersionPresets(ctx, templateVersionID) + if err != nil { + return nil, err + } + return presets, nil + }, +} + var GetAuthenticatedUser = Tool[NoArgs, codersdk.User]{ Tool: aisdk.Tool{ Name: ToolNameGetAuthenticatedUser, diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index 2e5e005883264..289b91a9657bf 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -170,6 +170,12 @@ func TestTools(t *testing.T) { } return agents }).Do() + preset := dbgen.Preset(t, store, database.InsertPresetParams{ + TemplateVersionID: r.TemplateVersion.ID, + Name: testutil.GetRandomNameHyphenated(t), + CreatedAt: r.TemplateVersion.CreatedAt, + Description: "Preset for agent tool tests.", + }) // Given: a client configured with the agent token. agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(r.AgentToken)) @@ -384,6 +390,20 @@ func TestTools(t *testing.T) { require.Empty(t, params) }) + t.Run("ListTemplateVersionPresets", func(t *testing.T) { + tb, err := toolsdk.NewDeps(memberClient) + require.NoError(t, err) + presets, err := testTool(t, toolsdk.ListTemplateVersionPresets, tb, toolsdk.ListTemplateVersionParametersArgs{ + TemplateVersionID: r.TemplateVersion.ID.String(), + }) + + require.NoError(t, err) + require.Len(t, presets, 1) + require.Equal(t, preset.ID, presets[0].ID) + require.Equal(t, preset.Name, presets[0].Name) + require.Equal(t, preset.Description, presets[0].Description) + }) + t.Run("GetWorkspaceAgentLogs", func(t *testing.T) { tb, err := toolsdk.NewDeps(memberClient) require.NoError(t, err) @@ -500,18 +520,34 @@ func TestTools(t *testing.T) { t.Run("CreateWorkspace", func(t *testing.T) { tb, err := toolsdk.NewDeps(client) require.NoError(t, err) - // We need a template version ID to create a workspace - res, err := testTool(t, toolsdk.CreateWorkspace, tb, toolsdk.CreateWorkspaceArgs{ - User: "me", - TemplateVersionID: r.TemplateVersion.ID.String(), - Name: testutil.GetRandomNameHyphenated(t), - RichParameters: map[string]string{}, + t.Run("WithoutPreset", func(t *testing.T) { + res, err := testTool(t, toolsdk.CreateWorkspace, tb, toolsdk.CreateWorkspaceArgs{ + User: "me", + TemplateVersionID: r.TemplateVersion.ID.String(), + Name: testutil.GetRandomNameHyphenated(t), + RichParameters: map[string]string{}, + }) + + require.NoError(t, err) + require.NotEmpty(t, res.ID, "expected a workspace ID") }) - // The creation might fail for various reasons, but the important thing is - // to mark it as tested - require.NoError(t, err) - require.NotEmpty(t, res.ID, "expected a workspace ID") + t.Run("WithPreset", func(t *testing.T) { + res, err := testTool(t, toolsdk.CreateWorkspace, tb, toolsdk.CreateWorkspaceArgs{ + User: "me", + TemplateVersionID: r.TemplateVersion.ID.String(), + TemplateVersionPresetID: preset.ID.String(), + Name: testutil.GetRandomNameHyphenated(t), + RichParameters: map[string]string{}, + }) + + require.NoError(t, err) + require.NotEmpty(t, res.ID, "expected a workspace ID") + + build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, res.LatestBuild.ID) + require.NotNil(t, build.TemplateVersionPresetID) + require.Equal(t, preset.ID, *build.TemplateVersionPresetID) + }) }) t.Run("WorkspaceSSHExec", func(t *testing.T) { @@ -1072,11 +1108,10 @@ func TestTools(t *testing.T) { { name: "WithPreset", args: toolsdk.CreateTaskArgs{ - TemplateVersionID: r.TemplateVersion.ID.String(), + TemplateVersionID: aiTV.TemplateVersion.ID.String(), TemplateVersionPresetID: presetID.String(), Input: "not enough barrel rolls", }, - error: "Template does not have a valid \"coder_ai_task\" resource.", }, }