@@ -2,7 +2,9 @@ package toolsdk
2
2
3
3
import (
4
4
"archive/tar"
5
+ "bytes"
5
6
"context"
7
+ "encoding/json"
6
8
"io"
7
9
8
10
"github.com/google/uuid"
@@ -20,28 +22,49 @@ type Deps struct {
20
22
AppStatusSlug string
21
23
}
22
24
23
- // HandlerFunc is a function that handles a tool call.
25
+ // HandlerFunc is atyped function that handles a tool call.
24
26
type HandlerFunc [Arg ,Ret any ]func (context.Context ,Deps ,Arg ) (Ret ,error )
25
27
28
+ // Tool consists of an aisdk.Tool and a corresponding typed handler function.
26
29
type Tool [Arg ,Ret any ]struct {
27
30
aisdk.Tool
28
31
Handler HandlerFunc [Arg ,Ret ]
29
32
}
30
33
31
- // Generic returns a type-erased version of the Tool.
32
- func (t Tool [Arg ,Ret ])Generic ()Tool [any ,any ] {
33
- return Tool [any ,any ]{
34
+ // Generic returns a type-erased version of a TypedTool where the arguments and
35
+ // return values are converted to/from json.RawMessage.
36
+ // This allows the tool to be referenced without knowing the concrete arguments
37
+ // or return values. The original TypedHandlerFunc is wrapped to handle type
38
+ // conversion.
39
+ func (t Tool [Arg ,Ret ])Generic ()GenericTool {
40
+ return GenericTool {
34
41
Tool :t .Tool ,
35
- Handler :func (ctx context.Context ,tb Deps ,args any ) (any ,error ) {
36
- typedArg , ok := args .( Arg )
37
- if ! ok {
38
- return nil ,xerrors .Errorf ("developer error: invalid argument type for tool %s " ,t . Tool . Name )
42
+ Handler :wrap ( func (ctx context.Context ,tb Deps ,args json. RawMessage ) (json. RawMessage ,error ) {
43
+ var typedArgs Arg
44
+ if err := json . Unmarshal ( args , & typedArgs ); err != nil {
45
+ return nil ,xerrors .Errorf ("failed to unmarshal args: %w " ,err )
39
46
}
40
- return t .Handler (ctx ,tb ,typedArg )
41
- },
47
+ ret ,err := t .Handler (ctx ,tb ,typedArgs )
48
+ var buf bytes.Buffer
49
+ if err := json .NewEncoder (& buf ).Encode (ret );err != nil {
50
+ return json.RawMessage {},err
51
+ }
52
+ return buf .Bytes (),err
53
+ },WithCleanContext ,WithRecover ),
42
54
}
43
55
}
44
56
57
+ // GenericTool is a type-erased wrapper for GenericTool.
58
+ // This allows referencing the tool without knowing the concrete argument or
59
+ // return type. The Handler function allows calling the tool with known types.
60
+ type GenericTool struct {
61
+ aisdk.Tool
62
+ Handler GenericHandlerFunc
63
+ }
64
+
65
+ // GenericHandlerFunc is a function that handles a tool call.
66
+ type GenericHandlerFunc func (context.Context ,Deps , json.RawMessage ) (json.RawMessage ,error )
67
+
45
68
type NoArgs struct {}
46
69
47
70
type ReportTaskArgs struct {
@@ -114,8 +137,8 @@ type UploadTarFileArgs struct {
114
137
}
115
138
116
139
// WithRecover wraps a HandlerFunc to recover from panics and return an error.
117
- func WithRecover [ Arg , Ret any ] (h HandlerFunc [ Arg , Ret ]) HandlerFunc [ Arg , Ret ] {
118
- return func (ctx context.Context ,tb Deps ,args Arg ) (ret Ret ,err error ) {
140
+ func WithRecover (h GenericHandlerFunc ) GenericHandlerFunc {
141
+ return func (ctx context.Context ,tb Deps ,args json. RawMessage ) (ret json. RawMessage ,err error ) {
119
142
defer func () {
120
143
if r := recover ();r != nil {
121
144
err = xerrors .Errorf ("tool handler panic: %v" ,r )
@@ -129,8 +152,8 @@ func WithRecover[Arg, Ret any](h HandlerFunc[Arg, Ret]) HandlerFunc[Arg, Ret] {
129
152
// This ensures that no data is passed using context.Value.
130
153
// If a deadline is set on the parent context, it will be passed to the child
131
154
// context.
132
- func WithCleanContext [ Arg , Ret any ] (h HandlerFunc [ Arg , Ret ]) HandlerFunc [ Arg , Ret ] {
133
- return func (parent context.Context ,tb Deps ,args Arg ) (ret Ret ,err error ) {
155
+ func WithCleanContext (h GenericHandlerFunc ) GenericHandlerFunc {
156
+ return func (parent context.Context ,tb Deps ,args json. RawMessage ) (ret json. RawMessage ,err error ) {
134
157
child ,childCancel := context .WithCancel (context .Background ())
135
158
defer childCancel ()
136
159
// Ensure that cancellation propagates from the parent context to the child context.
@@ -153,19 +176,18 @@ func WithCleanContext[Arg, Ret any](h HandlerFunc[Arg, Ret]) HandlerFunc[Arg, Re
153
176
}
154
177
}
155
178
156
- // wrapAll wraps all provided tools with the given middleware function.
157
- func wrapAll (mw func (HandlerFunc [any ,any ])HandlerFunc [any ,any ],tools ... Tool [any ,any ]) []Tool [any ,any ] {
158
- for i ,t := range tools {
159
- t .Handler = mw (t .Handler )
160
- tools [i ]= t
179
+ // wrap wraps the provided GenericHandlerFunc with the provided middleware functions.
180
+ func wrap (hf GenericHandlerFunc ,mw ... func (GenericHandlerFunc )GenericHandlerFunc )GenericHandlerFunc {
181
+ for _ ,m := range mw {
182
+ hf = m (hf )
161
183
}
162
- return tools
184
+ return hf
163
185
}
164
186
165
187
var (
166
188
// All is a list of all tools that can be used in the Coder CLI.
167
189
// When you add a new tool, be sure to include it here!
168
- All = wrapAll ( WithCleanContext , wrapAll ( WithRecover ,
190
+ All = [] GenericTool {
169
191
CreateTemplate .Generic (),
170
192
CreateTemplateVersion .Generic (),
171
193
CreateWorkspace .Generic (),
@@ -182,9 +204,9 @@ var (
182
204
ReportTask .Generic (),
183
205
UploadTarFile .Generic (),
184
206
UpdateTemplateActiveVersion .Generic (),
185
- ) ... )
207
+ }
186
208
187
- ReportTask = Tool [ReportTaskArgs ,string ]{
209
+ ReportTask = Tool [ReportTaskArgs ,codersdk. Response ]{
188
210
Tool : aisdk.Tool {
189
211
Name :"coder_report_task" ,
190
212
Description :"Report progress on a user task in Coder." ,
@@ -211,22 +233,24 @@ var (
211
233
Required : []string {"summary" ,"link" ,"state" },
212
234
},
213
235
},
214
- Handler :func (ctx context.Context ,tb Deps ,args ReportTaskArgs ) (string ,error ) {
236
+ Handler :func (ctx context.Context ,tb Deps ,args ReportTaskArgs ) (codersdk. Response ,error ) {
215
237
if tb .AgentClient == nil {
216
- return "" ,xerrors .New ("tool unavailable as CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE not set" )
238
+ return codersdk. Response {} ,xerrors .New ("tool unavailable as CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE not set" )
217
239
}
218
240
if tb .AppStatusSlug == "" {
219
- return "" ,xerrors .New ("workspace app status slug not found in toolbox" )
241
+ return codersdk. Response {} ,xerrors .New ("workspace app status slug not found in toolbox" )
220
242
}
221
243
if err := tb .AgentClient .PatchAppStatus (ctx , agentsdk.PatchAppStatus {
222
244
AppSlug :tb .AppStatusSlug ,
223
245
Message :args .Summary ,
224
246
URI :args .Link ,
225
247
State :codersdk .WorkspaceAppStatusState (args .State ),
226
248
});err != nil {
227
- return "" ,err
249
+ return codersdk. Response {} ,err
228
250
}
229
- return "Thanks for reporting!" ,nil
251
+ return codersdk.Response {
252
+ Message :"Thanks for reporting!" ,
253
+ },nil
230
254
},
231
255
}
232
256
@@ -934,9 +958,13 @@ The file_id provided is a reference to a tar file you have uploaded containing t
934
958
if err != nil {
935
959
return codersdk.TemplateVersion {},xerrors .Errorf ("file_id must be a valid UUID: %w" ,err )
936
960
}
937
- templateID ,err := uuid .Parse (args .TemplateID )
938
- if err != nil {
939
- return codersdk.TemplateVersion {},xerrors .Errorf ("template_id must be a valid UUID: %w" ,err )
961
+ var templateID uuid.UUID
962
+ if args .TemplateID != "" {
963
+ tid ,err := uuid .Parse (args .TemplateID )
964
+ if err != nil {
965
+ return codersdk.TemplateVersion {},xerrors .Errorf ("template_id must be a valid UUID: %w" ,err )
966
+ }
967
+ templateID = tid
940
968
}
941
969
templateVersion ,err := tb .CoderClient .CreateTemplateVersion (ctx ,me .OrganizationIDs [0 ], codersdk.CreateTemplateVersionRequest {
942
970
Message :"Created by AI" ,
@@ -1183,7 +1211,7 @@ The file_id provided is a reference to a tar file you have uploaded containing t
1183
1211
},
1184
1212
}
1185
1213
1186
- DeleteTemplate = Tool [DeleteTemplateArgs ,string ]{
1214
+ DeleteTemplate = Tool [DeleteTemplateArgs ,codersdk. Response ]{
1187
1215
Tool : aisdk.Tool {
1188
1216
Name :"coder_delete_template" ,
1189
1217
Description :"Delete a template. This is irreversible." ,
@@ -1195,16 +1223,18 @@ The file_id provided is a reference to a tar file you have uploaded containing t
1195
1223
},
1196
1224
},
1197
1225
},
1198
- Handler :func (ctx context.Context ,tb Deps ,args DeleteTemplateArgs ) (string ,error ) {
1226
+ Handler :func (ctx context.Context ,tb Deps ,args DeleteTemplateArgs ) (codersdk. Response ,error ) {
1199
1227
templateID ,err := uuid .Parse (args .TemplateID )
1200
1228
if err != nil {
1201
- return "" ,xerrors .Errorf ("template_id must be a valid UUID: %w" ,err )
1229
+ return codersdk. Response {} ,xerrors .Errorf ("template_id must be a valid UUID: %w" ,err )
1202
1230
}
1203
1231
err = tb .CoderClient .DeleteTemplate (ctx ,templateID )
1204
1232
if err != nil {
1205
- return "" ,err
1233
+ return codersdk. Response {} ,err
1206
1234
}
1207
- return "Successfully deleted template!" ,nil
1235
+ return codersdk.Response {
1236
+ Message :"Template deleted successfully." ,
1237
+ },nil
1208
1238
},
1209
1239
}
1210
1240
)