diff --git a/coderd/x/chatd/chattool/goal.go b/coderd/x/chatd/chattool/goal.go index 7d46e66dd4..6e360afec6 100644 --- a/coderd/x/chatd/chattool/goal.go +++ b/coderd/x/chatd/chattool/goal.go @@ -32,8 +32,8 @@ type GoalToolOptions struct { type getGoalArgs struct{} type completeGoalArgs struct { - GoalID uuid.UUID `json:"goal_id" description:"The expected current goal ID. The tool fails if the current goal changed."` - Summary string `json:"summary" description:"A concise non-empty summary of how the goal was completed."` + GoalID string `json:"goal_id" description:"The expected current goal ID. The tool fails if the current goal changed."` + Summary string `json:"summary" description:"A concise non-empty summary of how the goal was completed."` } type goalResult struct { @@ -74,9 +74,16 @@ func CompleteGoal(db database.Store, options GoalToolOptions) fantasy.AgentTool if !options.IsRootChat { return fantasy.NewTextErrorResponse("complete_goal can only be used from the root chat"), nil } - if args.GoalID == uuid.Nil { + goalIDStr := strings.TrimSpace(args.GoalID) + if goalIDStr == "" { return fantasy.NewTextErrorResponse("goal_id is required"), nil } + goalID, err := uuid.Parse(goalIDStr) + if err != nil { + return fantasy.NewTextErrorResponse( + xerrors.Errorf("invalid goal_id: %w", err).Error(), + ), nil + } summary := strings.TrimSpace(args.Summary) if summary == "" { return fantasy.NewTextErrorResponse("summary is required"), nil @@ -98,7 +105,7 @@ func CompleteGoal(db database.Store, options GoalToolOptions) fantasy.AgentTool } return err } - if current.ID != args.GoalID { + if current.ID != goalID { return sql.ErrNoRows } if current.Status != database.ChatGoalStatusActive { @@ -109,7 +116,7 @@ func CompleteGoal(db database.Store, options GoalToolOptions) fantasy.AgentTool } completed, err = tx.CompleteChatGoalByID(ctx, database.CompleteChatGoalByIDParams{ RootChatID: options.RootChatID, - ID: args.GoalID, + ID: goalID, CompletionSummary: sql.NullString{ String: summary, Valid: true, diff --git a/coderd/x/chatd/chattool/goal_test.go b/coderd/x/chatd/chattool/goal_test.go index 3cc6dd95e3..10518149d4 100644 --- a/coderd/x/chatd/chattool/goal_test.go +++ b/coderd/x/chatd/chattool/goal_test.go @@ -81,6 +81,17 @@ func TestGoalTools(t *testing.T) { require.Equal(t, database.ChatGoalStatusComplete, completedGoal.Status) } +func TestCompleteGoalSchemaUsesStringGoalID(t *testing.T) { + t.Parallel() + + tool := chattool.CompleteGoal(nil, chattool.GoalToolOptions{}) + info := tool.Info() + goalIDParam, ok := info.Parameters["goal_id"].(map[string]any) + require.True(t, ok) + require.Equal(t, "string", goalIDParam["type"]) + require.NotEqual(t, "array", goalIDParam["type"]) +} + func TestGetGoalReturnsNullWithoutCurrentGoal(t *testing.T) { t.Parallel() @@ -134,6 +145,16 @@ func TestCompleteGoalValidatesInput(t *testing.T) { input: `{"summary":"done"}`, message: "goal_id is required", }, + { + name: "empty goal id", + input: `{"goal_id":" ","summary":"done"}`, + message: "goal_id is required", + }, + { + name: "invalid goal id", + input: `{"goal_id":"not-a-uuid","summary":"done"}`, + message: "invalid goal_id", + }, { name: "empty summary", input: `{"goal_id":"00000000-0000-4000-8000-000000000001","summary":" "}`,